Graphics.DrawString weird abnormal kerning
Asked Answered
K

3

6

I am developing a label designing program and I have encountered a very weird issue that I cannot find an answer to anywhere on the internet.

The basic explanation is that when the layoutRectangle property of DrawString() is set to anything slightly higher than the size of the text length, The kerning seems to just turn off. It's best visible with a combination of characters 'Te'.

Correct Kerning with 'layoutRectangle' just enough to fit the text:
1

Wrong kerning with 'layoutRectangle' larger than required to fit the text:
2

You probably can spot the 'e' in the second image being drawn much further than in the first image. I have tried that on a new project, the same thing happens. You can observe the same behaviour even when not specifying the layoutRectangle at all.

Here is the line of code that draws the string:

canvasGraphics.Graphics.DrawString(
    _displayText.FormattedTextValue,
    new Font(FontName, (float)((drawFontSizeToUse / 72d) * Canvas.RenderDPI), fontStyle, GraphicsUnit.Pixel),
    Brushes.Black,
    (Rectangle)new RectangleD(ScreenBounds.X, ScreenBounds.Y, ScreenBounds.Width, ScreenBounds.Height),
    stringFormat
);

stringFormat just changes the alignment depending on the setting.

Does anyone know how to fix this issue? Thank you!

I have tried manipulating different StringFormat, TextRenderingHint and any Font options etc. (Although there might be some that I do not know of.)

I cannot use 'TextRenderer' as it cannot be used for printing.

Kinase answered 12/4, 2024 at 9:26 Comment(7)
Use TextRenderer.DrawText() instead.Narayan
@HansPassant I cannot. TextRenderer cannot be used for printing, and I require that functionality.Kinase
I know the "Te" looks wrong but the above sample shows the e to the right of the T I am sure if you change the "T" for a "L" you'd be happy.Tercet
@WalterVerhoeven It's not like I can, never use a 'T'.Kinase
You're just missing a method argument that tells you that output goes to the printer. If you don't want to add it then use Graphics.DpiY. If it is less than 300 then use TextRenderer. If you need an exact match between screen and paper then you'll have to use WPF instead.Narayan
@HansPassant well it's really drastic for me now to use WPF. This project is in development for a while. Is this really the only solution? As far as I know, even Microsoft says that TextRenderer cannot be used for printing, and yes it's 300 DPI and above, also TextRenderer requires Windows.Forms namespace, and for my core library I tried to avoid adding it as it should be a standalone library that can work with any Graphics component even if it's not Forms. And yes, I need exact match between paper and what's on screen.Kinase
@HansPassant, you can use a T just if it's a uppercase T then just move left 15% of the letters width, than you get what like to see, your in control where you print the lettersTercet
K
0

I just wanted to make sure this question is finished and is going to help someone down the line.

I want to thank @Ian Boyd and @dr.null for their answers, and they are really helpful for the future. I suggest anyone that is facing text drawing issues look at their answers and try to adapt their responses.

For my purposes, as I really do not want to change the way things are ran now, and I need printing functionality, I found a very simple fix to this issue and that is:

Add "\n" to the end of the string parameter passed onto the Graphics.DrawString. It's a ridiculous and an easy fix, but it works!

Kinase answered 16/4, 2024 at 8:26 Comment(0)
B
2

Both the Gdi and Gdi+ are important tools in WinForms and each has a use. They're pretty much all you have if you want to stick with this old technology and what it provides and supports. The point is that we need to know and understand when, where to use each, and why.

The rule is, TextRender.DrawText on controls and Graphics.DrawString on images and printings. Fine. However, in some scenarios this rule is revokable. For example, which one should be used to:

  • Draw a string on control vertically. (No rotation) Example
  • Draw a string on transparent surface and make the anti-aliasing blends the letters properly into the background. Example
  • Create ranges of characters to draw each with different color. For example, to highlight search words in ListBox, ListView, and DataGridView controls. Example
  • ...etc.

The answer is yours.


I understand that your code produces something like this when you resize the control:

SO78315327A

... and you want - for some unknown reason - to avoid using the TextRenderer to draw the text when none of the mentioned above scenarios apply in this context. Still, don't use Graphics.DrawString and use the GraphicsPath instead to fix this issue and get better quality overall.

public class SomeLabel : Label
{
    protected override void OnPaint(PaintEventArgs e)
    {            
        var cr = ClientRectangle;
            
        using (var gp = new GraphicsPath())
        using (var brFore = new SolidBrush(ForeColor))
        using (var sf = new StringFormat(StringFormat.GenericTypographic))
        using (var fnt = GetScaledFont(e.Graphics, Font, cr.Size, Text, 6, 128))
        {
            sf.FormatFlags |= StringFormatFlags.NoWrap;
            gp.AddString(Text, fnt.FontFamily, (int)fnt.Style, fnt.Size, cr, sf);
                
            e.Graphics.Clear(BackColor);
            e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
            e.Graphics.FillPath(brFore, gp);
        }
    }

    // Or your scaling logic ...
    private Font GetScaledFont(
        Graphics g,
        Font srcFont,
        Size canvasSize,
        string text,
        float minSize,
        float maxSize)
    {
        float sz = GetScaledFontSize(g, srcFont, canvasSize, text, minSize, maxSize);
        return new Font(srcFont.FontFamily, sz, srcFont.Style, GraphicsUnit.Pixel);
    }

    private float GetScaledFontSize(
        Graphics g,
        Font srcFont,
        Size canvasSize,
        string text,
        float minSize,
        float maxSize)
    {
        SizeF szf = g.MeasureString(text, srcFont);
        float hRatio = canvasSize.Height / szf.Height;
        float wRatio = canvasSize.Width / szf.Width;
        float scaleRatio = Math.Min(hRatio, wRatio);
        float scaledSize = Math.Max(minSize, Math.Min(maxSize, srcFont.Size * scaleRatio));
        return scaledSize / 72f * g.DpiY;
    }
}

The result is...

SO78315327B

Bereft answered 13/4, 2024 at 7:12 Comment(3)
So the reason why I can't use TextRenderer, as I have already stated is that as far as I know, it cannot be used for printing. (Please inform me otherwise if I am wrong!), also I think TextRenderer is max 300dpi, am I correct? Also, TextRenderer requires a reference to forms dll, which I want to avoid. But, thank you for the response I'll try that when I am back at work on monday!Kinase
@Kinase You need to differentiate between two things here. How to draw a control and how to print that out. The two routines are different. What you need to see on your screen is not necessarily what you need to send to the printer. You just need to know how to translate/transform what you see to get the desired printout. Now, yes reducing the references to libs is a good reason. Btw: you can print big posters with 300dpitif images. So, labels won't be a problem.Bereft
I see where you're coming from, but I think it's easier to render a one to one view of the label and the same view being actually passed onto the printer (I can pass the same rendering code to "PrintDocument" class and it's done) instead of trying to come up with some translating utility with yet additional steps. I guess you're right, 300 dpi for text might be fine, although we need to make sure barcodes are dpi perfect, so I guess it doesn't hurt to make sure text is too.Kinase
R
0

Short Version

Don't use Graphics.DrawString; it's slower, deprecated, and not recommended anymore.

Use TextRenderer.DrawText.

Graphics.DrawString TextRenderer.DrawString
bad good
the one we don't want to use the one we want to use
uses GDI+ for text rendering uses GDI for text rendering
graphics.MeasureString TextRenderer.MeasureText
graphics.DrawString TextRenderer.DrawText
Looks better
Localizes better
Faster

enter image description here

Long Version

There are two ways of drawing text in .NET:

  • GDI+ (graphics.MeasureString and graphics.DrawString)
  • GDI (TextRenderer.MeasureText and TextRenderer.DrawText)

From Michael Kaplan's (rip) excellent blog Sorting It All Out, In .NET 1.1 everything used GDI+ for text rendering. But there were some problems:

  • There are some performance issues caused by the somewhat stateless nature of GDI+, where device contexts would be set and then the original restored after each call.
  • The shaping engines for international text have been updated many times for Windows/Uniscribe and for Avalon (Windows Presentation Foundation), but have not been updated for GDI+, which causes international rendering support for new languages to not have the same level of quality.

So they knew they wanted to change the .NET framework to stop using GDI+'s text rendering system, and use GDI. At first they hoped they could simply change:

graphics.DrawString

to call the old DrawText API instead of GDI+. But they couldn't make the text-wrapping and spacing match exactly as what GDI+ did. So they were forced to keep graphics.DrawString to call GDI+ (compatiblity reasons; people who were calling graphics.DrawString would suddenly find that their text didn't wrap the way it used to).

A new static TextRenderer class was created to wrap GDI text rendering. It has two methods:

TextRenderer.MeasureText
TextRenderer.DrawText

Note: TextRenderer is a wrapper around GDI, while graphics.DrawString is still a wrapper around GDI+.

Bonus Reading

Ranita answered 12/4, 2024 at 17:32 Comment(5)
As I have already stated, I cannot use TextRenderer, it cannot be used for printing and it requires a reference to Forms which I want to avoid for my core library. Thank you for the explanation though!Kinase
You could P/Invoke the GDI API calls directly (or just copy-paste the implementation of TextRenderer - all 10 lines of it). It's just Windows API calls.Ranita
That's a good thought ! I'll give this a try if this might somehow work, although I am not entirely sure about TextRenderer being compatible with printing, but I'll play around with it and do some tests.Kinase
@Kinase Well when printing on Windows, when it comes down to it, it is still issuing GDI drawing calls, just rather than on a Device Context (DC) that represents the screen, or bitmap, it represents a printer. In the end, you can always use graphics.GetHDC to have the Graphics object cough-up a DC that you can then draw on (which is all that TextRenderer is doing). Yes, the WinAPI calls are more tedious, and old, but it's all .NET is doing.Ranita
In the end, Graphics.DrawString is actually calling out to the native GDI+ functions, while TextRenderer.DrawText is actually calling out to the native GDI functions. Now, yes, using GDI from the CLR means your application must be running on a platform that has GDI - you have to be running on Windows.Ranita
K
0

I just wanted to make sure this question is finished and is going to help someone down the line.

I want to thank @Ian Boyd and @dr.null for their answers, and they are really helpful for the future. I suggest anyone that is facing text drawing issues look at their answers and try to adapt their responses.

For my purposes, as I really do not want to change the way things are ran now, and I need printing functionality, I found a very simple fix to this issue and that is:

Add "\n" to the end of the string parameter passed onto the Graphics.DrawString. It's a ridiculous and an easy fix, but it works!

Kinase answered 16/4, 2024 at 8:26 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.