Recent

Author Topic: [SOLVED] Copy Printer.Canvas to a form object (PainBox?)  (Read 2341 times)

fatmonk

  • Sr. Member
  • ****
  • Posts: 252
[SOLVED] Copy Printer.Canvas to a form object (PainBox?)
« on: December 09, 2019, 03:51:05 pm »
I'm trying to redirect the Printer output to a 'print preview' object on my form.

I draw directly onto the Printer.Canvas as my report is relatively simply (in fact I think it would be more difficult to create using lazreport as it uses rotated text and all content just comes from the UI form rather than a database or other dataset).

My intention is to use the same code that sends to the printer BUT to then copy the Printer.Canvas to the Canvas of an object on my form to display as a print preview (in a TPaintBox?).

My working print code is as follows:

Code: [Select]
procedure TMainForm.doPrint(previewOnly: Boolean = false);
const
  LEFTMARGIN = 100;
  TOPMARGIN = 100;
  PADDING = 20;
  PAPERSIZENAME = 'A4';
var
  XPos, YPos, LineHeight, LineWidth: Int64;
  LeftSectionLeft, SectionCenter, RightSectionRight, SectionTop, SectionBottom, QRleft, QRtop, QRright, QRbottom: Int64;
  QRcodePosition: TRect;
begin

  with Printer do
  try
    try
      Refresh;
      PaperSize.PaperName:=PAPERSIZENAME;
      Orientation:=poLandscape;

      Title:='Print job title';

      LeftSectionLeft:=LEFTMARGIN;
      SectionTop:=TOPMARGIN;
      RightSectionRight:=PageWidth - LeftSectionLeft;
      SectionCenter:=Round((RightSectionRight-LeftSectionLeft)/2);
      SectionBottom:=PageHeight-SectionTop;

      //if (not(previewOnly)) then
      BeginDoc;

      // Setup the basics
      Canvas.Font.Name := 'Arial';
      Canvas.Font.Color := clBlack;
      Canvas.Font.Orientation:=0;
      Canvas.Brush.Color:= clNone;

//showMessage('printer.canvas.width='+IntToStr(Canvas.Width)+', .height='+IntToStr(Canvas.Height));


      // Draw all of the stuff on the canvas here

    except
      on E: EPrinter do ShowMessage('Printer Error: ' +  E.Message);
      on E: Exception do showMessage('Unexpected error when printing.');
    end;
  finally
    if (not(previewOnly)) then EndDoc;
    if (previewOnly) then
    begin
            ShowMessage('into print preview'+sLineBreak+'pbLabelpreview.Canvas.Width='+IntToStr(pbLabelpreview.Canvas.Width)+sLineBreak+'pbLabelpreview.Canvas.Height='+IntToStr(pbLabelpreview.Canvas.Height)+sLineBreak+'Printer.PageWidth='+IntToStr(Printer.PageWidth)+sLineBreak+'Printer.PageHeight='+IntToStr(Printer.PageHeight));

//      pbLabelpreview.Canvas.CopyRect(Classes.Rect(0, 0, pbLabelpreview.Width, pbLabelpreview.Height),
      pbLabelpreview.Canvas.CopyRect(Classes.Rect(0, 0, pbLabelpreview.Canvas.Width, pbLabelpreview.Canvas.Height),
        Printer.Canvas, Classes.Rect(0, 0, Printer.PageWidth, Printer.PageHeight));
      pbLabelPreview.Repaint;
      Printer.Abort;
    end;
  end;
end;

In the above pbLabelPreview is a TPaintBox on my main form.

As I say, this prints to the printer or to PDF (via 'Microsoft Print to PDF' device) as required.

I have a test button on my form that calls doPrint(true) to update the preview object...

This question seems to be trying to tackle a similar issue in a similar way, but my CopyRect doesn't seem to be doing anything - the TPaintBox remains blank.

I discovered that pbLabelpreview.Canvas.Width and pbLabelpreview.Canvas.Height are returning '0' (via the showMessage) so also tried using the dimensions of the PaintBox itself, but the PaintBox remains empty.

What am I doing wrong? Is there a better object to place on the form to have the Printer.Canvas copied onto - a TBitmap maybe?

Thanks,

FM
« Last Edit: January 03, 2020, 06:14:52 pm by fatmonk »

winni

  • Hero Member
  • *****
  • Posts: 3197
Re: Copy Printer.Canvas to a form object (PainBox?)
« Reply #1 on: December 09, 2019, 04:52:48 pm »
Hi!

Dont name a component  xxxLabelyyy if it is not an Label. That is confusing!

Is your "label" creted dynamic - then you must set the size!

Winni

lucamar

  • Hero Member
  • *****
  • Posts: 4219
Re: Copy Printer.Canvas to a form object (PainBox?)
« Reply #2 on: December 09, 2019, 05:13:49 pm »
I'm not sure but very possibly the printer canvas is write-only, so reading from it (to e.g. copy to another canvas) will probably return an empty one.

If I were you I would do it contrary-wise (drawing to the "preview" canvas and copying this one to the printer) or build a procedure accepting a Canvas object as parameter and generate the report to it; you could then either pass (or create on Nil) the preview PaintBox's canvas or the Printer one.
Turbo Pascal 3 CP/M - Amstrad PCW 8256 (512 KB !!!) :P
Lazarus/FPC 2.0.8/3.0.4 & 2.0.12/3.2.0 - 32/64 bits on:
(K|L|X)Ubuntu 12..18, Windows XP, 7, 10 and various DOSes.

fatmonk

  • Sr. Member
  • ****
  • Posts: 252
Re: Copy Printer.Canvas to a form object (PainBox?)
« Reply #3 on: December 09, 2019, 05:38:54 pm »
@winni It's a label in the real world that is being previewed!

@lucamer That was how I originally envisaged doing things, but a) I got the printer side working first and have struggled to get my head around how to flip it around, and b) when printing to a PDF using the printer canvas direct, it seems that the generated PDF contains text objects and image objects. I don't want to lose this latter functionality as by going via an intermediate canvas (is that likely?).

The post I linked post seems to suggest that it is possible to read from the Printer.Canvas...

When I tried to do things the other way around my stumbling block was sizing the 'preview canvas' correctly. Ultimately the real world labels will be printed to a sheet of A4 (and possibly Letter later on) paper, so pre-setting those on the Printer gets me a canvas of the correct size and proportions meaning that when I draw on the canvas I get the best quality possibly and everything fits correctly. I can't figure out how to do that with a disembodied canvas that is not derived from the printer page.

I don't get an error when I try to copy from it, but the results are different if I use a PaintBox (no change to the PaintBox after the copy) and when I use a TImage (I get a solid black image).

-FM

MichaelBM

  • New Member
  • *
  • Posts: 38
Re: Copy Printer.Canvas to a form object (PainBox?)
« Reply #4 on: December 09, 2019, 05:43:15 pm »
I have several reports in my CRM system. All reports are hard-coded using LazReport units. Nothing is designed using the LazReport UI.

All reports can be previewed on screen before printing. Works perfectly!

Some example code:

Code: Pascal  [Select][+][-]
  1.  
  2. with Sender as TlrCodeReport do begin
  3.     // Important. Before drawing, add a page
  4.     NewPage;
  5.     intPageNo := 1;
  6.  
  7.     // Set paper... 1=Letter 9=A4....
  8.     SetPaper(9, poPortrait);
  9.  
  10.     // working with mm
  11.     EnableMillimeters; // working in millimeters
  12.  
  13.     ShapeStyle.FrameColor := clBlack;
  14.     ShapeStyle.FrameWith := 0.1;
  15.  
  16.     DrawShape(20, 42, 180, 240, ShapeStyle); // Page border
  17.  
  18. ...
  19. ...
  20.     TextRectStyle.FontColor := clBlack;
  21.     SetFont('Tahoma', 9, [fsBold]);
  22.     TextOutXY(21, intLine, qryPrintData.FieldByName('CUSTOMERNAME').AsString);
  23.  
  24. ...
  25. ...
  26.  
  27.  
Development Tool: Lazarus 2.0.6 + FPC 3.0.4
Database: Firebird 2.5.7
Operating System: Windows 10 Pro 64-bit

fatmonk

  • Sr. Member
  • ****
  • Posts: 252
Re: Copy Printer.Canvas to a form object (PainBox?)
« Reply #5 on: December 09, 2019, 06:12:50 pm »
@MichaelBM Does LazReport support rotated text? I struggled to rotate text correctly using the LazReport UI...

I also need to be able to print variable length text within boxes. I do a lot of pre-calculation to work out the size of the box needed to contain text that is taken, for example, from 6 lines long and limited width, to a much wider string with few lines... and example may be better.

Input text:
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6

Output text:
Line 1, Line 2, Line 3, Line 4,
Line 5, Line 6.

Given the input text I work out, based on line height and width, and width available, what size box will fit the text best, draw the box and then add the text to the box.

Is this kind of thing possible with LazReport - the calculation based on calculated line width and height etc?

Using mm positioning may also be a problem - or at least more difficult - as I am doing everything based on half of the landscape page contaiing one set of content and the other half different content. thats works out nice and easy when I have direct access to the paper size...

I guess I could be worrying unnecessarily, I'd imagine LazReports is pretty powerful... I just don't fancy converting about 250 lines of code from drawing on a printer canvas to LazReport code.

Hmmm...

-FM

lucamar

  • Hero Member
  • *****
  • Posts: 4219
Re: Copy Printer.Canvas to a form object (PainBox?)
« Reply #6 on: December 09, 2019, 07:46:12 pm »
Drawing to an intermediate canvas (using for example a TBitmap or any other graphics, not a "raw" TCanvas) is really only a kluge for testing. It might work acceptbly for a real printer but, as you say, printing to a PDF (or similar) should be done properly.

The real solution is the second  one I offered (and as is done in the thread to which you linked): build a procedure accepting a Canvas object as parameter and generate the report in it.

Since you already have got printing to the printer done, all you need really do is extract to its own funciton the parts which are comon to any canvas and add another function to prepare the preview canvas. It shouldn't be too difficult.

The important thing to remember is that any "canvas" will behave more or less the same as any other so "printing" to, say, a TBitmap.Canvas is basically the same as "printing" to a TPrinter.Canvas, and the differences in the full process (e.g. dimensioning the bitmap for preview, calling Begin/EnDoc for the printer, etc.) can be put in their own separate procedures, say: LaunchPreview, LaunchPrint; which in turn call a procedure DrawToCanvas(SomeCanvas) where the common "printing" is done.

As for sizing the preview, it's a simple rule of three for both dimensions: if at "printer DPI" there are X pixels, then at "preview (screen) DPI" there should be ... In fact, anything you may "print" to diverse canvases should be sized accordingly, taking into account and adjusting for the origin and destination DPI: that's base graphics programing, though sometimes you can ignore it when origin and destination are the same device.
Turbo Pascal 3 CP/M - Amstrad PCW 8256 (512 KB !!!) :P
Lazarus/FPC 2.0.8/3.0.4 & 2.0.12/3.2.0 - 32/64 bits on:
(K|L|X)Ubuntu 12..18, Windows XP, 7, 10 and various DOSes.

MichaelBM

  • New Member
  • *
  • Posts: 38
Re: Copy Printer.Canvas to a form object (PainBox?)
« Reply #7 on: December 09, 2019, 08:22:39 pm »
@fatmonk: Yes, lrCodeReport supports a lot of fancy stuff.

Look in the sample files located here:

Code: Text  [Select][+][-]
  1. \Lazarus\components\lazreport\source\addons\lrcodereport\sample
Development Tool: Lazarus 2.0.6 + FPC 3.0.4
Database: Firebird 2.5.7
Operating System: Windows 10 Pro 64-bit

fatmonk

  • Sr. Member
  • ****
  • Posts: 252
Re: Copy Printer.Canvas to a form object (PainBox?)
« Reply #8 on: December 10, 2019, 04:04:24 pm »
@lucamar Thanks for the push in the right direction. Finally some progress...

When I first tried to modularise/procedurise the layout generation code last week I was trying the 'other' way around in that I had a function like function generateLayout(): TCanvas;. Switching things so that I feed in the canvas as a parameter now works perfectly.

My new procedure is: procedure generateLayoutOnCanvas(aCanvas: TCanvas; PageWidth, PageHeight: LongInt);.

I call this with:

Code: [Select]
procedure TMainForm.doPrint();
begin

  with Printer do
  try
    try
      Refresh;
      PaperSize.PaperName:=PAPERSIZENAME;
      Orientation:=poLandscape;

      Title:='My output doc';

      BeginDoc;

      generateLayoutOnCanvas(Canvas, Printer.PageWidth, Printer.PageHeight);

    except
      on E: EPrinter do ShowMessage('Printer Error: ' +  E.Message);
      on E: Exception do showMessage('Unexpected error when printing.');
    end;
  finally
    EndDoc;
  end;
end;

and printing to a physical printer or PDF works perfectly.

So, now onto the bit that's been causing me all the problems and I'm stuck again :-(

My plan is to include the print preview in the interface always, rather than have a print preview window (it may not be the most efficient, but it suits the use case for the application, so I'm giving it a go...)

To this end I have started with a function to update the preview that for the moment is triggered by a button click (I will work out the best time to update automatically later).

As a start I have the following:

Code: [Select]
procedure TMainForm.updatePreview();
var
  scaleFactorX, scaleFactorY: Double;
  previewXsize, previewYsize: Int64;
  myCanvas: TCanvas;
begin
  scaleFactorX:=Printer.XDPI / Screen.PixelsPerInch;
  scaleFactorY:=Printer.YDPI / Screen.PixelsPerInch;

  previewXsize:=Round(Printer.PageWidth/scaleFactorX);
  previewYsize:=Round(Printer.PageHeight/scaleFactorY);

//  showMessage('X-Scale = '+FloatToStr(scaleFactorX)+', Y-Scale = '+FloatToStr(scaleFactorY));

  try
    begin
      myCanvas.Create;
      generateLayoutOnCanvas(myCanvas, previewXsize, previewYsize);
    end;
  finally
    myCanvas.Free;
  end;

end;

Why myCanvas?

In order to use the auto-fit function of Canvas.CopyRect I'd like to draw the layout onto an interim canvas then copy that canvas into the canvas of an element on my form (which is best for this? a TBitMap or a TPaintBox or something else?).

As the above shows, I am calculating a scale factor between the printed page (which is working and laying out perfectly) and the size on screen - based on the DPI of both the printer and the screen. I am hoping that this means the text fits correctly as I ht problems wit hthsi last week when I first started playing with generating a preview with duplicate code).

However, again I am failing at the first step!

I get a SIGSEGV on the myCanvas.Create line!

Arrrgghhh... I'm obviously missing something fundamental again here, just when progress was being made.

-FM

lucamar

  • Hero Member
  • *****
  • Posts: 4219
Re: Copy Printer.Canvas to a form object (PainBox?)
« Reply #9 on: December 10, 2019, 04:25:32 pm »
My recomendation? Never use a raw canvas; instead use some graphic class (like TBitmap) and do anything you want in its canvas.

If for some reason you still want a raw canvas, at least create it properly, as any other object; instead of myCanvas.Create use: myCanvas := TCanvas.Create, so your code should be like:

Code: Pascal  [Select][+][-]
  1. procedure TMainForm.updatePreview();
  2. var
  3.   scaleFactorX, scaleFactorY: Double;
  4.   previewXsize, previewYsize: Int64;
  5.   myCanvas: TCanvas;
  6. begin
  7.   scaleFactorX:=Printer.XDPI / Screen.PixelsPerInch;
  8.   scaleFactorY:=Printer.YDPI / Screen.PixelsPerInch;
  9.  
  10.   previewXsize:=Round(Printer.PageWidth/scaleFactorX);
  11.   previewYsize:=Round(Printer.PageHeight/scaleFactorY);
  12.  
  13. //  showMessage('X-Scale = '+FloatToStr(scaleFactorX)+', Y-Scale = '+FloatToStr(scaleFactorY));
  14.  
  15.   myCanvas := TCanvas.Create;
  16.   try
  17.       generateLayoutOnCanvas(myCanvas, previewXsize, previewYsize);
  18.   finally
  19.     myCanvas.Free;
  20.   end;
  21. end;

Note that all "AnObject.Create" does is to call Create as any other method of AnObject (which is sometimes used to reinitialize an object), while making "AnObject := TAClass.Create" invokes the "magic" that reserves memory and creates the object's instance. The segmentation fault you see, then, is caused because you call a method of an object which doesn't exist yet.

HTH!

ETA: Oh, forgot about this:

[...] which is best for this? a TBitMap or a TPaintBox or something else?).

For something to show on a form use either a TImage or a TPaintBox. Since you're not generating the preview in the control's canvas but just copying it to it, I'd use  a TImage (maybe inside a TScrollBox), which is more suited to simply showing an image; TPaintBox is more useful when your painting operations are done directly to the control's canvas.
« Last Edit: December 10, 2019, 04:44:43 pm by lucamar »
Turbo Pascal 3 CP/M - Amstrad PCW 8256 (512 KB !!!) :P
Lazarus/FPC 2.0.8/3.0.4 & 2.0.12/3.2.0 - 32/64 bits on:
(K|L|X)Ubuntu 12..18, Windows XP, 7, 10 and various DOSes.

fatmonk

  • Sr. Member
  • ****
  • Posts: 252
Re: Copy Printer.Canvas to a form object (PainBox?)
« Reply #10 on: December 10, 2019, 06:21:54 pm »
Doh! "A dog returns to his vomit as a fool to his folly". I keep making that same mistake when trying to create objects.

Right, I have the TBitmap creating correctly and my generateLayoutOnCanvas function seems to accept it as a parameter, but I am now back to wheer I was last week with CopyRect() in that all I am getting is a solid black box in the image.

I just can't seem to get CopyRect to work.

This is what I have at the moment, with some feedback on the sizing and scaling calculations:

Code: [Select]
procedure TMainForm.updatePreview();
var
  scaleFactorX, scaleFactorY: Double;
  previewXsize, previewYsize: Int64;
  myBitMap: TBitmap;
begin
  scaleFactorX:=Printer.XDPI / Screen.PixelsPerInch;
  scaleFactorY:=Printer.YDPI / Screen.PixelsPerInch;

  previewXsize:=Round(Printer.PageWidth/scaleFactorX);
  previewYsize:=Round(Printer.PageHeight/scaleFactorY);

  showMessage('Printer PageWidth = '+FloatToStr(Printer.PageWidth)+', Printer PageHeight = '+FloatToStr(Printer.PageHeight)+sLineBreak+
              'X-Scale = '+FloatToStr(scaleFactorX)+', Y-Scale = '+FloatToStr(scaleFactorY)+sLineBreak+
              'Preview X-Size = '+FloatToStr(previewXsize)+', Preview Y-Size = '+FloatToStr(previewYsize));

  iLabelPreview.Width:=previewXsize;
  iLabelPreview.Height:=previewYsize;

  try
    begin
      myBitMap:=TBitmap.Create;
      generateLabelLayoutOnCanvas(myBitMap.Canvas, previewXsize, previewYsize);
      iLabelPreview.Canvas.CopyRect(Rect(0,0,iLabelPreview.Width,iLabelPreview.Height),
                                    myBitMap.Canvas,
                                    Rect(0,0,previewXsize,previewYsize));
//      iLabelPreview.Picture.Bitmap:=myBitMap;

    end;
  finally
    myBitMap.Free;
  end;

end;

(iLabelPreview is a TImage and it is in a TScrollBox as you suggested - I actually meant TImage when I asked which was best between TBitMap and TPaintBox, and already had them in a scrollbox so at least I was doing something right even if it was only in the interface design approach rather than the coding ;-P)

-FM

lucamar

  • Hero Member
  • *****
  • Posts: 4219
Re: Copy Printer.Canvas to a form object (PainBox?)
« Reply #11 on: December 10, 2019, 07:07:29 pm »
You should size the bitmap before trying to access its canvas, just as you do with iLabelPreview, so:

Code: Pascal  [Select][+][-]
  1.   myBitmap := TBitmap.Create;
  2.   try
  3.     myBitmap.Width := previewXSize;
  4.     myBitmap.Height := previewYSize;
  5.     {... rest of the code ...}

Note how the try ... finally block is built above: first create the object and then enter the try block. Otherwise, if Create fails it will jump to the finally block, where you then try to access members of an object which hasn't been created! :)

Also note that try...finally themselves delimit a block of statements, so you don't need to surround it further with begin...end
« Last Edit: December 10, 2019, 07:13:57 pm by lucamar »
Turbo Pascal 3 CP/M - Amstrad PCW 8256 (512 KB !!!) :P
Lazarus/FPC 2.0.8/3.0.4 & 2.0.12/3.2.0 - 32/64 bits on:
(K|L|X)Ubuntu 12..18, Windows XP, 7, 10 and various DOSes.

fatmonk

  • Sr. Member
  • ****
  • Posts: 252
Re: Copy Printer.Canvas to a form object (PainBox?)
« Reply #12 on: December 11, 2019, 05:07:03 pm »
@lucamar Thanks again.

Setting the size has got me 'working' to a point - I can see all of the content that I am writing to the canvas, but size and positioning is all over the place. I just have to work out the scaling part now as my hope that simply using the different DPIs as a scaling factor for page size has resulted in a nasty mess of drawn objects on the preview canvas. Some work (and deep thought) required there.

Re the create being outside of the try block.. that makes perfect sense, thanks. I don't use try blocks anywhere near enough, so I'm still getting my head around the nuances of their use - hopefully practice will make my code more robust. (and I'll get rid of those superfluous begin..end statements as well).

-FM

jamie

  • Hero Member
  • *****
  • Posts: 6090
Re: Copy Printer.Canvas to a form object (PainBox?)
« Reply #13 on: December 11, 2019, 11:00:43 pm »
I just create a Tbitmap with the same image Width and height of the selected printer..

Send it all to that bitmap and it should scale naturally ..

When viewing it to the screen, use a Stretch draw canvas function.

When ready to print, just send that same bitmap to the printer...

All done.

 I had to do this because I have code that renders images to a canvas and when I get ready to send it to the printer I found out that not all drawing functions are supported on the printing device.
 
 Things like rounded corners etc.., so by using a intermediate bitmap all these issues are gone..
The only true wisdom is knowing you know nothing

 

TinyPortal © 2005-2018