2

I have a simple TForm named Form1; Image1 which is a TImage loaded with a PNGImage and a Button1 TButton to test things. It was implemented sucessfully a method to AlphaBlend Image1's picture. Code follows:

procedure SetPNGOpacity(Image : TImage; Alpha: Byte);
var
    Bmp: TBitmap;
    BlendFn: TBlendFunction;
    PNG: TPNGImage;
begin
    Png := TPngImage.Create;
    Png.Assign(TPNGImage(Image.Picture.Graphic));
    Bmp := TBitmap.Create;
    Bmp.Assign(Png);
    Image.Picture.Bitmap.PixelFormat := pf32bit;
    Image.Picture.Bitmap.AlphaFormat := afPremultiplied;
    Image.Picture.Bitmap.Canvas.Brush.Color := clBlack;
    Image.Picture.Bitmap.SetSize(Png.Width, Png.Height);
    BlendFn.BlendOp := AC_SRC_OVER;
    BlendFn.BlendFlags := 0;
    BlendFn.SourceConstantAlpha := Alpha;
    BlendFn.AlphaFormat := AC_SRC_ALPHA;
    winapi.windows.AlphaBlend(
        Image.Picture.Bitmap.Canvas.Handle,
        0, 0, Image.Picture.Bitmap.Width,
        Image.Picture.Bitmap.Height,
        Bmp.Canvas.Handle,
        0, 0, Bmp.Width,
        Bmp.Height,
        BlendFn
    );
    Bmp.FreeImage;
    Bmp.Free;
    Png.Free;
end;

If I simple calls this on the Button1 onClick the Image is blended. My goal anyway is to Fade In/Out Image1; or in other words, go to Opacity 0 to 255 and inverse way. What I could see is that the SetPNGOpacity up there stop working inside a Loop. I naturaly tried set the application busy with the following code:

procedure TForm1.Button1Click(Sender: TObject);
var 
    I : integer;
begin
    I := 255;
    while I > 0 do
    begin
        I := I - 1;
        sleep(125);
        SetPNGOpacity(Image2, I);
   //     MessageBeep(0);
    end;
end;

I was just expecting to wait some seconds with a inactive window and then Image1 should desappear completelly. What did not happen. So I tried it with a simple thread to Fade Out, descripted here:

TBar = class(TThread)
private
    I : integer;
public
    procedure execute; override;
    procedure Test;
    constructor Create;
end;

implementation

constructor TBar.Create;
begin
    inherited Create(false);
    I := 255;
end;

procedure TBar.execute;
begin
    while I > 0 do
    begin
        I := I - 1;
        sleep(250);
        synchronize(Test);
     //   MessageBeep(0);
    end;
end;

procedure TBar.Test;
begin
    SetPNGOpacity(Form1.Image2, I);
end;

And call it like this:

procedure TForm1.Button1Click(Sender: TObject);
var 
    Foo : TBar;
begin
    Foo := TBar.Create;
end;

Again, nothing happens. So I need you guys again. Someone have an idea about it? Am I doing something wrong? Does anyone know some useful reading; or even a helpful piece of code? Note: I really wish it would be using TImage or even a TBitmap which I could "extract/store" in a TImage.

Thanks in advance.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
Guill
  • 350
  • 5
  • 17
  • 3
    You could leave latter discussions behind... You are the only one here complaining aboute "the right way". Just to clarefy, I'm working on the same project, with the same kind of restriction. I will try to be as flexible as possible. Please, stop treating me like a rebel. But anyway, what you mean by "blend it"? – Guill Feb 14 '14 at 22:44
  • Have a look at this answer to the question [`Fade an image using GDI+ (i.e. Change only the Alpha channel of a TGPGraphic)`](http://stackoverflow.com/a/13706077/576719). – LU RD Feb 14 '14 at 23:26
  • @David Few people calls it "right" but let's nevermind this. You already know why would be hard to use `Canvas.Draw` don't you? I'm not discarding it. I'm just asking you. About the diference I know. But I'm trying to "abstract" (don't know if it is the better word) it to my user. He doesn't need to know about, specially, these things. @LU This routine is not applicable to PNGImage, is it? – Guill Feb 14 '14 at 23:43
  • Load the image once and once only. Why would you load it over and over again? When you need to draw at a new opacity, do so. Keep hold of the original image. Draw a blended image somewhere else. I've been telling you that for a week now. – David Heffernan Feb 14 '14 at 23:45
  • @Guill, perhaps not, but the technique used for the fading effect can be applied here as well. You have one image, one paintbox and a timer. The painting is done in the paintbox `OnPaint` event and the timer alters state and calls paintbox.invalidate. – LU RD Feb 14 '14 at 23:51
  • @David If I store the Bitmap in runtime and draw the alpha blended image in TImage's canvas it should work. I will still need to learn about paiting stuff because I can't see how to update that canvas once I don't know how to clean it. – Guill Feb 14 '14 at 23:51
  • @LURD that timer stuff would not freeze the application if I set a little longer time? I don't have much experience with TTimer. – Guill Feb 14 '14 at 23:54
  • The timer is the easiest way to solve this without freezing. Used in many visual components for animation. – LU RD Feb 14 '14 at 23:56
  • Why have a TImage? You don't need that. – David Heffernan Feb 14 '14 at 23:58
  • @LURD I'll give the shot. – Guill Feb 14 '14 at 23:59
  • @David in what Canvas I suppose to paint? – Guill Feb 15 '14 at 00:00

2 Answers2

5

There are three main problems for why your approach is not working (I haven't looked at the threaded part).

  1. You don't give a chance for the application to process the messages that would reflect the change in the image. This is mentioned in the now deleted answer. For testing purposes, you can insert an Application.ProcessMessages call in each iteration. Ultimately, you would like to use a timer for animation purposes. Depending on your needs it may need to be something with a higher resolution than the TTimer.

  2. You are not rendering from the same image every time. This is mentioned in the comments as not keeping an original image to render from. Right after the first iteration your image has been changed, and when you grab the image out of it to use as the source consecutively, it doesn't look anything like the previous source.

  3. You are not blending on the same target every time. The first time round you render the image on a blank-black bitmap. With each iteration, the target you're blending on to changes to something else.

The below is not my recommendation but what would be modified for your approach to see it work. The foremost important thing IMO you should do is that, render it wherever you like but keep your original image unmodified, not in a TImage but in a TPngImage of its own f.i..

procedure SetPNGOpacity(Master: TBitmap; Image : TImage; Alpha: Byte);
begin
    Image.Picture.Bitmap.PixelFormat := pf32bit;
    Image.Picture.Bitmap.AlphaFormat := afPremultiplied;
    Image.Picture.Bitmap.Canvas.Brush.Color := clBlack;
    Image.Picture.Bitmap.SetSize(Master.Width, Master.Height);
    Image.Picture.Bitmap.Canvas.FillRect(Rect(0, 0, Master.Width, Master.Height));
    Image.Picture.Bitmap.Canvas.Draw(0, 0, Master, Alpha); // thanks to TLama for telling that Canvas.Draw has an optional opacity parameter in later Delphi versions
end;

procedure TForm1.Button1Click(Sender: TObject);
var
    Bmp: TBitmap;
    I : integer;
begin
    Bmp := TBitmap.Create;
    Bmp.Assign(TPNGImage(Image2.Picture.Graphic));
    I := 255;
    while I > 0 do
    begin
        I := I - 1;
        SetPNGOpacity(Bmp, Image2, I);
        Application.ProcessMessages;
        Sleep(10);
   //     MessageBeep(0);
    end;
    Bmp.Free;
end;
Sertac Akyuz
  • 54,131
  • 4
  • 102
  • 169
  • To use TPNGImage I would need to replace TImage in all my current code. If I see that it is possible I can get rid of that amount of Creates. I'll study TPNGImage a bit and come back. – Guill Feb 15 '14 at 02:12
  • Where do you suggest I draw all my PGNImages? – Guill Feb 15 '14 at 02:19
  • @Guill - A paintbox would do fine. – Sertac Akyuz Feb 15 '14 at 02:20
  • 1
    @Sertac, `Image.Picture.Bitmap.Canvas.Draw(0, 0, Master, Alpha)` instead of `AlphaBlend` ? It better fits to the rest of the code there :-) [self-destructive comment...] – TLama Feb 15 '14 at 02:22
  • @Sertac, I've had also hard times to adopt the `Opacity` parameter. [another self-destructive comment...] – TLama Feb 15 '14 at 02:25
  • There is anything else I should set to create a `TPaintBox`, than any other component? I did it the default way and could not see my image. [Code here](http://pastebin.com/N2UU2v2p). (`Self` is a Form) – Guill Feb 15 '14 at 02:35
  • @Guill - It's different, it doesn't have any permanent image like the TImage. Normally you paint in its OnPaint event handler - when you need it to paint something you invalidate or repaint it. But you can use it's canvas outside the paint handler too. It may take some time to get used to.. – Sertac Akyuz Feb 15 '14 at 02:40
  • I see. But the code up there should not have shown my image? – Guill Feb 15 '14 at 02:42
  • @Guill - Replace the contents of your 'SetPNG..' with these two lines: `Form1.PaintBox1.Canvas.FillRect(Rect(0, 0, 105, 105));` `Form1.PaintBox1.Canvas.Draw(0, 0, Master, Alpha);` of course adjust the rect. – Sertac Akyuz Feb 15 '14 at 02:47
  • Well, I tried with my runtime PaintBox, nothing happens. I created one at designtime and it shows my image but no opacity changes. – Guill Feb 15 '14 at 02:57
  • @Guill - I now tried on a blank new project (XE2). Design time paintbox, image and a button, load a png to the image at design time, replace the function with the two lines in the comment. I'm seeing the partial transparent, fading image OK. – Sertac Akyuz Feb 15 '14 at 03:03
  • @SertacAkyuz I find that plain `TPaintBox` is flickery like this. Do you notice that too. Making a windowed version appears to deal with the issue. Do you have a better cure for the flicker? – David Heffernan Feb 15 '14 at 16:22
  • @David - yes, more often than not it is flickering. I think avoiding erasing (using off-screen bitmap) could help a little. Maybe using a more accurate timer instead of looping with message processing also? How do you window it? Put it on a panel? Sorry, saw it now! – Sertac Akyuz Feb 15 '14 at 16:43
  • It's the erasing that is the problem, as always. But that's hard to avoid in a non-windowed control. I think. – David Heffernan Feb 15 '14 at 16:50
  • @David - instead of accurately calculating transparency, I'd try accurate timing - with repaint instead of invalidate. – Sertac Akyuz Feb 15 '14 at 16:56
  • 1
    @Guill - I'm surprised that information/advice here in the answer and comments were not enough to sort this out. – Sertac Akyuz Feb 15 '14 at 18:35
  • @Sertac The same information has been gone over again and again in the previous Qs. The issue is that Guill still did not understand the Windows paint cycle as evidenced by the pastebin in comments to my answer. I think it took a complete working program to convince Guill. – David Heffernan Feb 15 '14 at 22:16
  • @David - I get it, thanks. Now when you tell me like this, it even rang a bell (fasm). :) – Sertac Akyuz Feb 16 '14 at 01:24
4

At the risk of sounding like a broken record, you are going about this the wrong way. A TImage is useful for a static image – it's the wrong thing to use to show anything dynamic. What you need to do is:

  1. Load your image into a TBitmap or TPNGImage or some such TGraphic descendent.
  2. Put a TPaintBox onto your form.
  3. Run a timer that ticks at the desired refresh rate.
  4. From the timer call Invalidate or perhaps Refresh on the paint box.
  5. Add an OnPaint handler for the paint box that paints your dynamic image.

The code looks like this:

type
  TForm1 = class(TForm)
    PaintBox1: TPaintBox;
    Timer1: TTimer;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure PaintBox1Paint(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
  private
    FBitmap: TBitmap;
    FOpacity: Integer;
  end;

procedure TForm1.FormCreate(Sender: TObject);
var
  Png: TPngImage;
begin
  Png := TPngImage.Create;
  Try
    Png.LoadFromFile('C:\desktop\YoshiMarioParty9.png');
    FBitmap := TBitmap.Create;
    FBitmap.Assign(Png);
  Finally
    Png.Free;
  End;

  BorderIcons := [biSystemMenu, biMinimize];
  BorderStyle := bsSingle;
  PaintBox1.Align := alClient;
  ClientWidth := FBitmap.Width;
  ClientHeight := FBitmap.Height;

  Timer1.Interval := 1000 div 25; // 25Hz refresh rate
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  Timer1.Enabled := False;
  FBitmap.Free;
end;

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  inc(FOpacity, 5);
  PaintBox1.Invalidate;
end;

procedure TForm1.PaintBox1Paint(Sender: TObject);
begin
  PaintBox1.Canvas.Brush.Color := clWhite;
  PaintBox1.Canvas.Brush.Style := bsSolid;
  PaintBox1.Canvas.FillRect(PaintBox1.ClientRect);
  PaintBox1.Canvas.Draw(0, 0, FBitmap, FOpacity);
end;

This results in a reasonable result, but there is flicker. This can be eliminated by setting the form's DoubleBuffered property to True, but I'd prefer a better solution to that.

This approach to solving the flicker is to make the paint box a windowed control. The VCL TPaintBox is a non-windowed control and so paints on its parent's window. This does tend to lead to flicker. So, here's a version with a simple paint box control derived from TCustomControl. This variant sets everything up at run time because I've not bother registering the paint box control as a design time control, although it's perfectly simple to do so.

program PaintBoxDemo;

uses
  Classes, Graphics, Controls, Forms, ExtCtrls, Diagnostics, pngimage;

type
  TWindowedPaintBox = class(TCustomControl)
  private
    FOnPaint: TNotifyEvent;
  protected
    procedure Paint; override;
  public
    constructor Create(AOwner: TComponent); override;
    property Canvas;
  published
    property Align;
    property Anchors;
    property Color;
    property Constraints;
    property DragCursor;
    property DragKind;
    property DragMode;
    property Enabled;
    property Font;
    property ParentColor;
    property ParentFont;
    property ParentShowHint;
    property PopupMenu;
    property ShowHint;
    property Touch;
    property Visible;
    property OnClick;
    property OnContextPopup;
    property OnDblClick;
    property OnDragDrop;
    property OnDragOver;
    property OnEndDock;
    property OnEndDrag;
    property OnGesture;
    property OnMouseActivate;
    property OnMouseDown;
    property OnMouseEnter;
    property OnMouseLeave;
    property OnMouseMove;
    property OnMouseUp;
    property OnPaint: TNotifyEvent read FOnPaint write FOnPaint;
    property OnStartDock;
    property OnStartDrag;
  end;

constructor TWindowedPaintBox.Create(AOwner: TComponent);
begin
  inherited;
  ControlStyle := ControlStyle + [csReplicatable];
  Width := 105;
  Height := 105;
end;

procedure TWindowedPaintBox.Paint;
begin
  Canvas.Font := Font;
  Canvas.Brush.Color := Color;
  if csDesigning in ComponentState then
  begin
    Canvas.Pen.Style := psDash;
    Canvas.Brush.Style := bsClear;
    Canvas.Rectangle(0, 0, Width, Height);
  end;
  if Assigned(FOnPaint) then
    FOnPaint(Self);
end;

var
  Form: TForm;
  PaintBox: TWindowedPaintBox;
  Timer: TTimer;
  Bitmap: TBitmap;
  Stopwatch: TStopwatch;

type
  TEventHandlers = class
    class procedure TimerHandler(Sender: TObject);
    class procedure PaintHandler(Sender: TObject);
  end;

class procedure TEventHandlers.TimerHandler(Sender: TObject);
begin
  PaintBox.Invalidate;
end;

class procedure TEventHandlers.PaintHandler(Sender: TObject);
var
  t: Double;
  Opacity: Integer;
begin
  t := Stopwatch.ElapsedMilliseconds;
  Opacity := Trunc(128.0*(1.0+Sin(t/300.0)));
  PaintBox.Canvas.Brush.Color := clWhite;
  PaintBox.Canvas.Brush.Style := bsSolid;
  PaintBox.Canvas.FillRect(PaintBox.ClientRect);
  PaintBox.Canvas.Draw(0, 0, Bitmap, Opacity);
end;

procedure BuildForm;
var
  Png: TPngImage;
begin
  Png := TPngImage.Create;
  Try
    Png.LoadFromFile('C:\desktop\YoshiMarioParty9.png');
    Bitmap := TBitmap.Create;
    Bitmap.Assign(Png);
  Finally
    Png.Free;
  End;

  PaintBox := TWindowedPaintBox.Create(nil);
  PaintBox.Parent := Form;
  PaintBox.Align := alClient;
  PaintBox.DoubleBuffered := True;
  PaintBox.OnPaint := TEventHandlers.PaintHandler;

  Timer := TTimer.Create(nil);
  Timer.Interval := 1000 div 25; // 25Hz refresh rate
  Timer.Enabled := True;
  Timer.OnTimer := TEventHandlers.TimerHandler;

  Form.Caption := 'PaintBox Demo';
  Form.BorderIcons := [biSystemMenu, biMinimize];
  Form.BorderStyle := bsSingle;
  Form.ClientWidth := Bitmap.Width;
  Form.ClientHeight := Bitmap.Height;
  Form.Position := poScreenCenter;

  Stopwatch := TStopwatch.StartNew;
end;

procedure TidyUp;
begin
  Timer.Free;
  PaintBox.Free;
  Bitmap.Free;
end;

begin
  Application.MainFormOnTaskbar := True;
  Application.CreateForm(TForm, Form);
  BuildForm;
  Application.Run;
  TidyUp;
end.

This is a GUI program contained in a single file, which is obviously not the way to write production code. I just do it like this here to make it possible for you to paste the code into a .dpr file verbatim and so prove to yourself that this approach works.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • You did not implement an OnPaint event handler. You should use the exact same code as I did for the paint box. Or use a standard one and worry about flicker later. – David Heffernan Feb 15 '14 at 17:50
  • I want just to show the image. The OnPaint is needed even without the fading thing? – Guill Feb 15 '14 at 17:52
  • You need to re-read my answer here http://stackoverflow.com/questions/21630970/ . You don't seem to fully grasp the event driven nature of painting. – David Heffernan Feb 15 '14 at 18:00
  • @Guill - What you're gonna create and destroy in the constructor (e.g. 'PNG'), don't make it a field. Make it a local var. – Sertac Akyuz Feb 16 '14 at 01:26
  • @David What value is better to replace in that line `Opacity := Trunc(128.0*(1.0+Sin(t/300.0)));` to get the fade slower/faster? – Guill Feb 19 '14 at 04:16