Recent

Author Topic: [Solved] Can't access a canvas.  (Read 9231 times)

Hopestation

  • Full Member
  • ***
  • Posts: 181
[Solved] Can't access a canvas.
« on: September 20, 2014, 12:02:24 pm »
Hi.

I am using Lazarus version 1.2.4, FPC version 2.6.4 in Windows XP Pro.

The following example is a simple form with a button and a timer.

The Button's Onclick calls FormCreate.

unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, Controls, Dialogs, ExtCtrls, Forms, Graphics, LCLIntf, LCLType,
  SysUtils;

type

  { Tfrm_1 }

  Tfrm_1 = class(TForm)
    Button1: TButton;
   Timer: TTimer;
    procedure FormCreate(Sender: TObject);
    procedure TimerTimer(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  frm_1: Tfrm_1;

implementation

{$R *.lfm}

procedure Tfrm_1.FormCreate(Sender: TObject);
begin
  Canvas.Pen.Color := clBlack;
  Canvas.Pen.Width := 5;
  Canvas.MoveTo(Random(ClientWidth), Random(ClientHeight));
  Canvas.LineTo(Random(ClientWidth), Random(ClientHeight));
end;

procedure Tfrm_1.TimerTimer(Sender: TObject);
begin
  Canvas.Pen.Color := clRed;
  Canvas.Pen.Width := 1;
  Canvas.MoveTo(Random(ClientWidth), Random(ClientHeight));
  Canvas.LineTo(Random(ClientWidth), Random(ClientHeight));
end;
End.

Random lines are draw when the timer is enabled, but the single line in the FormCreate procedure is never drawn.

Every time I click on the button a line is drawn

Why doesn't drawing on the canvas work in the initial FormCreate procedure but works when the button click calls it?

Thanks.
« Last Edit: September 22, 2014, 08:09:43 pm by Hopestation »

taazz

  • Hero Member
  • *****
  • Posts: 5368
Re: Can't access a canvas.
« Reply #1 on: September 20, 2014, 12:42:27 pm »
Don't do that and by that I mean to draw things outside the onpaint event.
It is unwise to draw outside the paint event for two reasons
  1) They are not persistent and are going to be lost in the next redraw.
  2) in some widget sets drawing outside the paint will raise an exception.

Try it out for your self, don't use a timer use a button to draw random lines, press it a couple of times to draw a few of them and them move the window half outside the visible screen and bring it back again, you should see that what ever was outside the visible screen is now cleared. With out anything in the onpaint event to redraw those lines they are lost.

Good judgement is the result of experience … Experience is the result of bad judgement.

OS : Windows 7 64 bit
Laz: Lazarus 1.4.4 FPC 2.6.4 i386-win32-win32/win64

Mike.Cornflake

  • Hero Member
  • *****
  • Posts: 1260
Re: Can't access a canvas.
« Reply #2 on: September 20, 2014, 01:05:59 pm »
Exact what @Taazz said.

Looking at your code, you think you're responsible for deciding when to draw on your form.  You're not, Windows is.  You don't know if another app is in front of yours.  Windows does.   Windows will tell you when your form needs updating by posting a WM_PAINT message to your app, which Lazarus LCL intercepts on your behalf and calls the TForm.OnPaint event.

Now, Windows doesn't know when your app needs to change what's being drawn, so won't always send you a WM_PAINT event when you want it.  So instead; when you need to change what you're drawing, you need to tell Windows that an update is needed.  (And here I get a little vague).  You either call TForm.Invalidate, TForm.Refresh or TForm.Repaint.   I never know which, and simply try one then the other until it works :)

And your final issue is putting draw code in FormCreate.  At that stage, the form isn't even visible.  So what happens is - you draw on the canvas (which might not even be assigned a handle yet, but assume it does).  The form is then shown.  Windows sends a WM_PAINT message.  You've no OnPaint handler, so the LCL intercepts the WM_PAINT and paints the form on your behalf with default setting - essentially filling the window with clDefault, overwriting all your Canvas draw stuff.

So, move all your drawing code to the forms OnPaint event.   If what you're drawing changes regularly, then, yeah, use a Timer, but all your OnTimer code is going to do is call one of Invalidate; Refresh; or Repaint;  (Yeah, I don't do a lot of drawing code, others will let you know the difference :))  That way, Windows knows your form needs repainting, and next time part of your Form is visible (or immediately if it is visible), it will send a WM_PAINT message.

This may help
http://stackoverflow.com/questions/1251009/whats-the-difference-between-refresh-update-repaint

Welcome to the wonderful world of Windows Messaging :)

Oh.  Drawing on the TForm itself is rarely a good idea.  That's where you put all your controls, and you may end up drawing underneath those controls.  If you only need a specific area being painted, then consider using a TPaintBox instead.  Everything mentioned above is still applicable, but instead of TForm.Invalidate etc, you'll call TPaintBox.Invalidate;  And TPaintBox has it's own OnPaint event.
 
« Last Edit: September 20, 2014, 01:10:38 pm by Mike.Cornflake »
Lazarus Trunk/FPC Trunk on Windows [7, 10]
  Have you tried searching this forum or the wiki?:   http://wiki.lazarus.freepascal.org/Alternative_Main_Page
  BOOKS! (Free and otherwise): http://wiki.lazarus.freepascal.org/Pascal_and_Lazarus_Books_and_Magazines

felipemdc

  • Administrator
  • Hero Member
  • *
  • Posts: 3538
Re: Can't access a canvas.
« Reply #3 on: September 20, 2014, 01:51:44 pm »
In my oppinion it is no problem to draw in the form directly. But the drawing should always be in the OnPaint event like already said. You can have a TBitmap to store your image that you want to build progressively and then in the OnPaint event draw that image to the form.

In the other events you can draw to that MyBitmap.Canvas ... you can draw to a bitmap at any time. Only drawing to controls should be only in the OnPaint event. To trigger an update of the form call Form.Invalidate(); this will generate an OnPaint event at the right time.

Mike.Cornflake

  • Hero Member
  • *****
  • Posts: 1260
Re: Can't access a canvas.
« Reply #4 on: September 20, 2014, 01:59:57 pm »
Quote
In my oppinion it is no problem to draw in the form directly.

I wasn't saying it was a problem, I was saying it was rarely a good idea - I meant that from a design decision POV.  How many RealWorld(tm) apps have you produced or seen that have code in the Form.OnPaint?  I'm not saying it doesn't happen, just that I've never seen it.  Most of the time what you need is a new control with all the drawing code in there, or a TPaintBox that is positioned specifically on the form.

But yeah, that's just my two cents.  From a technical point of view, there's no problem with using TForm.OnPaint.   

And yeah, caching the paints with a TBitmap is always a good idea.  I was sure that either the TForm or TPaintBox had a DoubleDebuffer option, but I'm just not seeing it.  So yeah, doing the buffering yourself will save you from flickering and decrease overall paint times.
« Last Edit: September 20, 2014, 09:47:18 pm by felipemdc »
Lazarus Trunk/FPC Trunk on Windows [7, 10]
  Have you tried searching this forum or the wiki?:   http://wiki.lazarus.freepascal.org/Alternative_Main_Page
  BOOKS! (Free and otherwise): http://wiki.lazarus.freepascal.org/Pascal_and_Lazarus_Books_and_Magazines

Hopestation

  • Full Member
  • ***
  • Posts: 181
Re: Can't access a canvas.
« Reply #5 on: September 20, 2014, 02:37:03 pm »
Thanks for your replies.

This simple form was an attempt to understand why I couldn't send an image and text to an array of panels which I created at design time.

Panels don't have an OnPaint event. Do I use the form's OnPaint event to display all the panels in one go?

Thanks.
« Last Edit: September 20, 2014, 02:41:46 pm by Hopestation »

Mike.Cornflake

  • Hero Member
  • *****
  • Posts: 1260
Re: Can't access a canvas.
« Reply #6 on: September 20, 2014, 03:02:44 pm »
Correct.  Panels don't have a OnPaint by design, they're designed as container controls really.  You don't need to do any painting on them to display them.  They're handled by the control.   
What is your need?   
Easiest solution if you're playing is to drop a TPaintBox on each panel and use that OnPaint.
Alternatively, if you're trying to display an image, you have TImage to use...
Alternatively, depending on your need, you may want to consider creating a new control - descended from TGraphicControl, and implementing that...  As I say, depends on your needs...
Lazarus Trunk/FPC Trunk on Windows [7, 10]
  Have you tried searching this forum or the wiki?:   http://wiki.lazarus.freepascal.org/Alternative_Main_Page
  BOOKS! (Free and otherwise): http://wiki.lazarus.freepascal.org/Pascal_and_Lazarus_Books_and_Magazines

howardpc

  • Hero Member
  • *****
  • Posts: 4144
Re: Can't access a canvas.
« Reply #7 on: September 20, 2014, 03:39:05 pm »
Panels don't have an OnPaint event. Do I use the form's OnPaint event to display all the panels in one go?

Panels don't have a published OnPaint event, so you can't set it in the Object Inspector. They have a public OnPaint event which you can assign in code, such as in the form's OnCreate event. As in the following

Code: [Select]
unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, ExtCtrls, Forms, Controls, Graphics;

type
  TForm1 = class(TForm)
    Panel1: TPanel;
    procedure FormCreate(Sender: TObject);
  private
    procedure PanelOnPaint(Sender: TObject);
  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
  Panel1.OnPaint:=@PanelOnPaint;
  Randomize;
end;

procedure TForm1.PanelOnPaint(Sender: TObject);
var
  p: TPanel;
  r: TRect;
begin
  p:=TPanel(Sender);
  r:=p.ClientRect;
  p.Canvas.Pen.Width:=2;
  P.Canvas.Pen.Color:=Random(clWhite);
  p.Canvas.Line(r);
  P.Canvas.Pen.Color:=Random(clWhite);
  p.Canvas.Arc(r.Left, r.Top, r.Right, r.Bottom, r.Right, r.Bottom, (r.Right-r.Left) div 2, r.Top);
end;

end.

felipemdc

  • Administrator
  • Hero Member
  • *
  • Posts: 3538
Re: Can't access a canvas.
« Reply #8 on: September 20, 2014, 09:54:55 pm »
I wasn't saying it was a problem, I was saying it was rarely a good idea - I meant that from a design decision POV.  How many RealWorld(tm) apps have you produced or seen that have code in the Form.OnPaint?  I'm not saying it doesn't happen, just that I've never seen it.  Most of the time what you need is a new control with all the drawing code in there, or a TPaintBox that is positioned specifically on the form.

Yes, you are right, I'd never do it like that because the right design is to use a TCustomControl descendent so that the code will be modular and reusable.

But for a newbie struggling it might be ok ... but eventually he should read the docs for the best way of doing this: http://wiki.freepascal.org/Developing_with_Graphics#Create_a_custom_control_which_draws_itself

Hopestation

  • Full Member
  • ***
  • Posts: 181
Re: Can't access a canvas.
« Reply #9 on: September 21, 2014, 09:36:42 pm »
Thanks for all you answers.

I have now gone back to my original project. This has a panel filling the form, on which I add at design time a number of panels, on which an image and labels are produced. I have used the main form's OnPaint event to add the images to the child panels and draw lines on the main panel.

When I run the program the main panel and its child panels appear, but the images and lines are missing. I have a menu item, Page Setup, which calls the OnPaint event and clicking this draws the images and lines. If I collapse the window and resize it the images and lines have gone and I have to click Page Setup again to retrieve them.

I have tried Mike's suggestions of TForm.Invalidate, TForm.Refresh and TForm.Repaint but none made any difference.

Below is my code:

unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, Menus,
  ExtCtrls, StdCtrls;

type

  { TForm1 }

  TForm1 = class(TForm)
    Image1: TImage;
    Label1: TLabel;
    Label2: TLabel;
    Label3: TLabel;
    Label4: TLabel;
    Label5: TLabel;
    MainMenu1: TMainMenu;
    MnuPgSetup: TMenuItem;
    MnuQuit: TMenuItem;
    MnuFile: TMenuItem;
    Panel1: TPanel;
    PnlPage: TPanel;
    procedure FormCreate(Sender: TObject);
    procedure FormPaint(Sender: TObject);
    procedure MnuQuitClick(Sender: TObject);
  private
    { private declarations }
  public
    { public declarations }
  end;

var
  Form1: TForm1;
  LGO : TBitMap;
  Panl: Array[1..10] of TPanel;
  Derb: Array[1..10] of TLabel;
  WClb: Array[1..10] of TLabel;
  Nam: Array[1..10] of TLabel;
  Stat: Array[1..10] of TLabel;
  Posn: Array[1..10] of TLabel;
  GapH, GapV, LineY, N, NumPnl, PanlWd, PanlHt, PicW, PicH,
  PageHt, PageWd, TextY: Integer;
  PgScale: Real;
implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
begin
  Top := 0;
  Left := 0;
  PageHt := 297;
  PageWd := 210;
  NumPnl := 10;

  Height := Screen.Height - 28;
  Width  := Trunc(PageWd * Screen.Height / PageHt);

  PnlPage.Top := 0;
  PnlPage.Left := 0;
  PnlPage.Height := Height - 17;
  PnlPage.Width := ClientWidth;

  PgScale := PnlPage.Height / PageHt;

  GapH := Trunc(5 * PgScale);
  GapV := 2 * GapH;

  PanlWd := (PnlPage.Width - (2 * GapV)) Div 2;
  PanlHt := (PnlPage.Height - (5 * GapV)) Div 5;

  LineY  := PanlHt Div 20;

  LGO := TBitMap.Create;
  LGO.LoadFromFile('DDWC Logo.bmp');

  PicH    := PanlHt  Div 3;
  PicW    := PicH * LGO.Width Div LGO.Height;

  For N :=  1 to NumPnl Do
  begin
    Panl[N]:=TPanel.Create(self);
    Panl[N].Parent:=PnlPage;
    Panl[N].Left := GapH;
    Panl[N].Top:= GapH + (N - 1) Div 2 * (GapV + PanlHt);
    Panl[N].Width:= PanlWd;
    Panl[N].Height:= PanlHt;
    Panl[N].color:= clWhite;
    Panl[N].BevelOuter := bvNone;

    Panl[N].Canvas.Pen.color:= clBlack;
    Panl[N].Font.Size := 14;

    Derb[N]:=TLabel.Create(self);
    Derb[N].Parent:=Panl[N];
    Derb[N].Left := PicW;
    Derb[N].Top  := 5;
    Derb[N].Autosize := False;
    Derb[N].Width := PanlWd - PicW;
    Derb[N].Height := 22;
    Derb[N].Font.Name := 'Times New Roman';
    Derb[N].Font.Size := 16;
    Derb[N].Alignment := taCenter;
    Derb[N].Caption := 'Derbyshire Dales';

    WClb[N]:=TLabel.Create(self);
    WClb[N].Parent:=Panl[N];
    WClb[N].Left := PicW;
    WClb[N].Top  := Derb[N].Top + Derb[N].Height;
    WClb[N].Autosize := False;
    WClb[N].Width := PanlWd - PicW;
    WClb[N].Height := 25;
    WClb[N].Font.Name := 'Times New Roman';
    WClb[N].Font.Size := 16;
    WClb[N].Alignment := taCenter;
    WClb[N].Caption := 'Woodcraft Club';

    Nam[N]:=TLabel.Create(self);
    Nam[N].Parent:=Panl[N];
    Nam[N].Left := 0;
    Nam[N].Top  := WClb[N].Top + WClb[N].Height;
    Nam[N].Autosize := False;
    Nam[N].Width := PanlWd;
    Nam[N].Height := 18;
    Nam[N].Font.Name := 'Woodbadge';
    Nam[N].Font.Size := 12;
    Nam[N].Alignment := taCenter;
    Nam[N].Font.color := clMaroon;
    Nam[N].Caption := 'Joe Bloggs';

    Stat[N]:=TLabel.Create(self);
    Stat[N].Parent:=Panl[N];
    Stat[N].Left := 0;
    Stat[N].Top  := Nam[N].Top + Nam[N].Height;
    Stat[N].Autosize := False;
    Stat[N].Width := PanlWd;
    Stat[N].Height := 25;
    Stat[N].Font.Name := 'kredit';
    Stat[N].Font.Size := 12;
    Stat[N].Alignment := taCenter;
    Stat[N].Font.color := clGreen;
    Stat[N].Caption := 'Club';

    Posn[N]:=TLabel.Create(self);
    Posn[N].Parent:=Panl[N];
    Posn[N].Left := 0;
    Posn[N].Top  := Stat[N].Top + Stat[N].Height;
    Posn[N].Autosize := False;
    Posn[N].Width := PanlWd;
    Posn[N].Height := 25;
    Posn[N].Font.Name := 'kredit';
    Posn[N].Font.Size := 12;
    Posn[N].Font.color := clTeal;
    Posn[N].Alignment := taCenter;
    Posn[N].Caption := 'Member';
  end;
//  Invalidate;
//  Refresh;
  Repaint;
end;

procedure TForm1.FormPaint(Sender: TObject);
begin
  PnlPage.Canvas.Pen.Color := clMedGray;
  PnlPage.Canvas.Pen.Style := psDash;
  For N :=  1 to NumPnl  Do
  begin
    if N Mod 2 = 0 then Panl[N].Left := 3 * GapH + PanlWd;
    PnlPage.Canvas.Line(0, Panl[N].Top - 1, PnlPage.Width, Panl[N].Top - 1);
    PnlPage.Canvas.Line(0, Panl[N].Top + PanlHt, PnlPage.Width, Panl[N].Top + PanlHt);
    Panl[N].Canvas.StretchDraw(Bounds(5, 5, PicW + 5, PicH + 5), LGO);
  end;
  PnlPage.Canvas.Line(GapH - 1, 0, GapH - 1, PnlPage.Height);
  PnlPage.Canvas.Line(PanlWd + GapH, 0, PanlWd + GapH, PnlPage.Height);
  PnlPage.Canvas.Line(Panl[N].Left - 1, 0, Panl[N].Left - 1, PnlPage.Height);
  PnlPage.Canvas.Line(2*PanlWd + 3*GapH, 0, 2*PanlWd + 3*GapH, PnlPage.Height);
end;

procedure TForm1.MnuQuitClick(Sender: TObject);
begin
  Halt;
end;

END.

I have attached a picture of the initial screen and after clicking Page Setup.

Have I missed something?

Mike.Cornflake

  • Hero Member
  • *****
  • Posts: 1260
Re: Can't access a canvas.
« Reply #10 on: September 21, 2014, 10:18:06 pm »
You've put the code in FormPaint, but are painting on Panels.   You need to match the OnPaint with the control, so in this case each TPanel will need their own OnPaint (or hook them all to the same OnPaint and use the TPanel.Tag property to work out which panel needs painting...). 

(Windows will tell your form each individual control that needs painting in addition to the form, so in this case Windows tells your app to paint a panel, that panel has no OnPaint handler (see howardpc's suggestion above), so Lazarus paints your panel for you, back to base color.

Also, please place all your code inside [ code ]   ... [ /code ] tags so the forum displays them better (remove the spaces inside the [ ], I just put them there so the forum didn't think I was trying to display code)
« Last Edit: September 21, 2014, 10:41:29 pm by Mike.Cornflake »
Lazarus Trunk/FPC Trunk on Windows [7, 10]
  Have you tried searching this forum or the wiki?:   http://wiki.lazarus.freepascal.org/Alternative_Main_Page
  BOOKS! (Free and otherwise): http://wiki.lazarus.freepascal.org/Pascal_and_Lazarus_Books_and_Magazines

Hopestation

  • Full Member
  • ***
  • Posts: 181
Re: Can't access a canvas.
« Reply #11 on: September 22, 2014, 08:09:08 pm »
Thanks, Mike and Howardpc.

Using Howardpc's code solved the problem.

 

TinyPortal © 2005-2018