0

I am writing a set of routines to print my visual component (TStringGrid descendant) with an ability to see its print preview. The preview is done using TPaintBox. I want that drawing utilized my component font.

So, basically I have several procedures:

  • Measuring all required sizes taking Printer page size and the component font as a basis.

  • Drawing the component. It takes a Canvas as input parameter. I want this to be a common drawing procedure for Printer.Canvas and TPaintBox.Canvas.

  • Scaling the TPaintBox drawing using SetGraphicsMode and SetWorldTransform so that it considers screen DPI / printer DPI ratio (in fact, 96 / 600).

procedure InitDrawingParams;
var
  TextHeight: Integer;
begin
  FPageWidth := Printer.PageWidth;
  FPageHeight := Printer.PageHeight;
  
  //actually, calculating the scale is a bit more complex, 
  //but this is just a simplification to illustrate the idea
  FScale := 96 / 600; 
  
  Printer.Canvas.Font.Assign(MyStringGrid.Font); //in fact, Tahoma, 8
  
  TextHeight := Printer.Canvas.TextHeight('I');
  FDataOffset := TextHeight div 5;
  FRowHeight := TextHeight + FDataOffset;
  
  PaintBox.Width := Round(FPageWidth * FScale);
  PaintBox.Height := Round(FPageHeight * FScale); 
end;

procedure DrawPage(ACanvas: TCanvas);
var
  R: TRect;
  PageMargin, RectWidth, RectHeight: Integer;
begin
  PageMargin := FPageWidth div 25;

  //Drawing the rectangle at the top of the page 
  //and fill it with the text:
  
  RectWidth := FPageWidth - 2 * PageMargin;
  RectHeight := FRowHeight;

  ACanvas.Rectangle(
    PageMargin, 
    PageMargin, 
    PageMargin + RectWidth, 
    PageMargin + RectHeight
  );

  R.left := PageMargin + FDataOffset;
  R.top := PageMargin;
  R.right := PageMargin + RectWidth - FDataOffset;
  R.bottom := PageMargin + RectHeight;

  ACanvas.Font.Assign(MyStringGrid.Font);
  
  DrawText(
    ACanvas.Handle, 
    PChar(MyStringGrid.Cells[0, 0]), 
    -1, 
    R, 
    DT_SINGLELINE or DT_VCENTER or DT_NOPREFIX
  );
end;

procedure TMyForm.PaintBoxPaint(Sender: TObject);
var
  XForm: TXForm;
  XFormOld: TXForm;
  GMode: Integer;
  dc: HDC;
begin
  dc := PaintBox.Canvas.Handle;

  XForm.eM11 := FScale;
  XForm.eM12 := 0;
  XForm.eM21 := 0;
  XForm.eM22 := FScale;
  XForm.eDx := 0;
  XForm.eDy := 0;

  GMode := SetGraphicsMode(dc, GM_ADVANCED);
  try
    if GetWorldTransform(dc, XFormOld) then
    try
      SetWorldTransform(dc, XForm);

      PaintBox.Canvas.Brush.Color := clWhite;
      PaintBox.Canvas.FillRect(Rect(0, 0, FPageWidth, FPageHeight));

      DrawPage(PaintBox.Canvas);
    finally
      SetWorldTransform(dc, XFormOld);
    end;
  finally
    SetGraphicsMode(dc, GMode);
  end;
end;

Everything is perfect when I start printing and call DrawPage(Printer.Canvas). But DrawPage(PaintBox.Canvas) draws very small text. It is about 96 / 600 smaller than I expected. The rectangle is drawn correctly in both ways.

Using SetGraphicsMode and SetWorldTransform, I expected that I will have a common drawing procedure and all needed scaling for rectangle and text will be done automatically.

I also tried to adjust the Font.Height like

ACanvas.Font.Height := Round(ACanvas.Font.Height / FScale);

but the result is very poor especially when I change the size of PaintBox thus changing the FScale.

So, my questions:

  1. Is it possible to have one universal drawing procedure for printing and for previewing?

  2. What am I missing in my code and how it can be improved to render the font correctly?

UPDATE:

Just discovered that

Printer.Canvas.Font.Assign(MyStringGrid.Font);

and

PaintBox.Canvas.Font.Assign(MyStringGrid.Font);

work differently. In the first case, printer font height is adjusted automatically probably due to different printer PixelsPerInch and the grid PixelsPerInch (or, more precisely, PixelsPerInch of their fonts), while there is no such a difference between the grid Canvas and PaintBox Canvas.

That's why I get so small font in the preview.

So, it seems, when drawing to the PaintBox.Canvas, it is absolutely necessary to add this code after the font assigning:

if Preview then
  ACanvas.Font.Height := 
    MulDiv(
      ACanvas.Font.Height, 
      FPrinterPixelsPerInchX, 
      Screen.PixelsPerInch
    );

With those lines, the font in the preview mode is scaled more or less well. But another problem appears:

When drawing the text over the previously drawn cell rectangle, the text margins sometimes overlap the rectangle's border, especially when the preview size gets smaller.

enter image description here

For larger preview resolution, everything is much better:

enter image description here

How can I fix that for smaller resolution?

4
  • Not sure if it is important, but do you have a 4k screen and maybe Windows scaling is set to something else than 100%? Commented Aug 27 at 21:22
  • @DelphiCoder It's 1920x1080 and Windows scaling was125%. Changing the scaling to 100% did not affect the problem. Commented Aug 28 at 1:02
  • Why are you setting world transform back to the default value. Wouldn't that undo the world transformation you have just done? When looking at example of Using Coordinate Spaces and Transformations the world transformation is never set back to the original value. Commented Aug 28 at 8:19
  • @SilverWarior I tried to comment setting it back (both ModifyWorldTransform and SetGraphicsMode or one of them), and I see no change in the behavior. I think, setting it back cannot change the drawing done prior to that call. Commented Aug 28 at 8:40

1 Answer 1

0

So, I have tried 3 different solutions so far:

  1. Using automatic preview scaling using SetGraphicsMode and SetWorldTransform and a 'common' procedure for printer canvas and screen canvas. It is a perfect way if you use either geometric figures and images without text rendering or just the text output without geometric figures and images. If you have a mix of both, using method is problematic.

  2. Using SetMapMode(Canvas.Handle, MM_ANISOTROPIC) along with SetWindowExtEx and SetViewportExtEx . The approach, cons and pros are very similar to the first method. It seems, it is possible two combine these two methods, I saw this in the internet but did not try myself and don't understand the sense.

  3. Do not use the automatic scaling like 1 and 2. Instead, calculate ratio between the screen DPI and printer DPI and adjust this ratio if your preview size is varying (i.e. you use Zoom In / Zoom Out). Probably, it's better to calculate two separate ratios: one for font size/height and another one for other drawing objects. In the drawing procedure, use this ratios wherever you calculate font sizes, positions and distances. In the end, I chose this method.

I have rewritten two of my above mentioned procedures. In DrawPage, I added two parameters: IsPreview and PreviewScale. In PaintBoxPaint , everything is much simpler now:

procedure DrawPage(ACanvas: TCanvas; IsPreview: Boolean; PreviewScale: Double);
var
  R: TRect;
  PageMargin, RectWidth, RectHeight: Integer;
begin
  if IsPreview then
  begin
    //Recalculate some basic parameters.

    //a new global variable; for printer, it will be 1
    FOutputRatio := FPrnPixelsPerInchX / Screen.PixelsPerInch * PreviewScale;

    PageMargin := Round(FPageWidth * PreviewScale) div 25;

    ACanvas.Font := MyStringGrid.Font;
    ACanvas.Font.Height := Round(ACanvas.Font.Height * FOutputRatio);

    TextHeight := ACanvas.TextHeight('I');
    FDataOffset := TextHeight div 5;
    FRowHeight := TextHeight + FDataOffset;
  end
  else
  begin
    PageMargin := FPageWidth div 25;
  end;
  
  //Drawing the rectangle at the top of the page 
  //and fill it with the text:
  
  RectWidth := Round((FPageWidth - 2 * PageMargin) * PreviewScale);
  RectHeight := Round(FRowHeight * PreviewScale);

  ACanvas.Rectangle(
    PageMargin, 
    PageMargin, 
    PageMargin + RectWidth, 
    PageMargin + RectHeight
  );

  R.left := PageMargin + FDataOffset;
  R.top := PageMargin;
  R.right := PageMargin + RectWidth - FDataOffset;
  R.bottom := PageMargin + RectHeight;

  ACanvas.Font.Assign(MyStringGrid.Font);
  
  DrawText(
    ACanvas.Handle, 
    PChar(MyStringGrid.Cells[0, 0]), 
    -1, 
    R, 
    DT_SINGLELINE or DT_VCENTER or DT_NOPREFIX
  );
end;

procedure TMyForm.PaintBoxPaint(Sender: TObject);
begin
  PaintBox.Canvas.Brush.Color := clWhite;
  PaintBox.Canvas.FillRect(Rect(0, 0, PaintBox.Width, PaintBox.Height));

  DrawPage(PaintBox.Canvas, True, FScale);
end;

When you are making scaling yourself, it sometimes worth using the output gamma correction. In simple words, when you are zooming out your preview, your output page should look a bit lighter. If your reduced preview looks unnaturally dark, consider to use the gamma correction. There can be different approaches here, for example, gray-scaling the whole output image or adjusting dark colors like Canvas.Pen.Color by adding a few bit to its RGB value.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.