locked
SetTextCharacterExtra Winapi is not working with C# RRS feed

  • Question

  • I want to increase/decrease space between characters while drawing using System.Drawing in C#. I am using one winapi SetTextCharacterExtra to do so but it is not working at all. Below is my code block:

    [DllImport("gdi32.dll", CharSet=CharSet.Auto)] 
    public
     static extern int SetTextCharacterExtra(

    IntPtr hdc, // DC handle int nCharExtra // extra-space value ); public void Draw(Graphics g) { IntPtr hdc = g.GetHdc(); SetTextCharacterExtra(hdc, 24); g.ReleaseHdc(hdc); e.Graphics.DrawString("str",this.Font,Brushes.Black,0,0); }

    I have tried printing each character manually but it is not working as perfect as it should be. Any idea on this please?


    A.R.Prajapati


    Friday, May 29, 2020 2:59 AM

Answers

  • I tried to adapt your code and use SetWorldTransform to keep same x,y for GDI and GDI+, but it doesn't work (blue text (GDI) should be at same position as black text (GDI+))...
    And the fonts have not exactly the same size

    Test code =>

    var inputString = "This is drawn by ";
    var fontSize = 31f;
    //var font = new Font("Avenir", fontSize, new FontStyle());
    // var font = new Font("Times New Roman", fontSize, new FontStyle());
    var font = new Font("Avenir Black", fontSize, new FontStyle());
    
    var startX1 = 50;
    var startY1 = 50;
    // Draw string by Graphics
    e.Graphics.DrawString(inputString + "DrawString", font, new SolidBrush(Color.Black), startX1, startY1);
    
    // Draw string by ExtTextOut
    LOGFONT lf = new LOGFONT();
    font.ToLogFont(lf, e.Graphics);
    
    IntPtr hPrinterDC = e.Graphics.GetHdc();
    StringBuilder sbText = new StringBuilder(inputString + "ExtTextOut");
    if (hPrinterDC != IntPtr.Zero)
    {
        int nPrintHorzRes = GetDeviceCaps(hPrinterDC, HORZRES);
        int nPrintVertRes = GetDeviceCaps(hPrinterDC, VERTRES);
        int nPhysWidth = GetDeviceCaps(hPrinterDC, PHYSICALWIDTH);
        int nPhysHeight = GetDeviceCaps(hPrinterDC, PHYSICALHEIGHT);
        int nPhysOffsetX = GetDeviceCaps(hPrinterDC, PHYSICALOFFSETX);
        int nPhysOffsetY = GetDeviceCaps(hPrinterDC, PHYSICALOFFSETY);
    
        IntPtr hDCScreen = GetDC(IntPtr.Zero);
        float nLogPixelsXScreen = GetDeviceCaps(hDCScreen, LOGPIXELSX);
        float nLogPixelsYScreen = GetDeviceCaps(hDCScreen, LOGPIXELSY);
        ReleaseDC(hDCScreen, IntPtr.Zero);
        float nLogPixelsXPrinter = GetDeviceCaps(hPrinterDC, LOGPIXELSX);
        float nLogPixelsYPrinter = GetDeviceCaps(hPrinterDC, LOGPIXELSY);
    
        float nScaleX = Math.Max(nLogPixelsXScreen, nLogPixelsXPrinter) / Math.Min(nLogPixelsXScreen, nLogPixelsXPrinter);
        float nScaleY = Math.Max(nLogPixelsYScreen, nLogPixelsYPrinter) / Math.Min(nLogPixelsYScreen, nLogPixelsYPrinter);
    
        System.Drawing.Drawing2D.Matrix transform = new System.Drawing.Drawing2D.Matrix();
        transform.Scale(nScaleX, nScaleY);
        XFORM renderTransform = new XFORM();
        var elements = transform.Elements;
        var m11 = elements[0];
        var m12 = elements[1];
        var m21 = elements[2];
        var m22 = elements[3];
        var dx = elements[4];
        var dy = elements[5];
    
        renderTransform.eM11 = (float)m11;
        renderTransform.eM12 = (float)m12;
        renderTransform.eM21 = (float)m21;
        renderTransform.eM22 = (float)m22;
        // Test offsets for (50, 50) showing font sizes are different when they start at same positions
        // int nOffsetX = 34;
        // int nOffsetY = -14;
        int nOffsetX = 0;
        int nOffsetY = 0;
        renderTransform.eDx = (float)transform.OffsetX + nOffsetX;
        renderTransform.eDy = (float)transform.OffsetY + nOffsetY;
        SetGraphicsMode(hPrinterDC, GM_ADVANCED);
        //SetMapMode(hPrinterDC, MM_LOENGLISH);
        SetMapMode(hPrinterDC, MM_TEXT);
        bool bRet = SetWorldTransform(hPrinterDC, ref renderTransform);
    
        var startX2 = startX1;
        var startY2 = startY1;
        RECT rc = new RECT(startX2, startY2, nPhysWidth, nPhysHeight);
    
        //Rectangle margins = e.MarginBounds;
        //// inches => mm => pixels
        //int nLeftPixels = MulDiv(margins.Left, 2540, 100000);
        //nLeftPixels = MulDiv(nLeftPixels, (int)nLogPixelsXScreen, 72);
        //startX2 += nLeftPixels;
    
        //// inches => mm => pixels
        //int nTopPixels = MulDiv(margins.Top, 2540, 100000);
        //nTopPixels = MulDiv(nTopPixels, (int)nLogPixelsYScreen, 72);
        //startY2 += nTopPixels;
    
        //rc.left = startX2;
        //rc.top = startY2;
    
        SetViewportOrgEx(hPrinterDC, -nPhysOffsetX, -nPhysOffsetY, IntPtr.Zero);
    
        //lf.lfHeight = MulDiv((int)-font.Size, (int)nLogPixelsYPrinter, 72);
        lf.lfHeight = (int)(lf.lfHeight / nScaleY);   
    
        IntPtr hFontNew = CreateFontIndirect(lf);
        IntPtr hFontOld = SelectObject(hPrinterDC, hFontNew);
    
        SetBkMode(hPrinterDC, TRANSPARENT);
        SetTextColor(hPrinterDC, ColorTranslator.ToWin32(Color.Blue));      
        ExtTextOut(hPrinterDC, rc.left, rc.top, ETO_OPAQUE, IntPtr.Zero, sbText, (uint)sbText.Length, IntPtr.Zero);
    
        rc.top += -lf.lfHeight;
        SetTextColor(hPrinterDC, ColorTranslator.ToWin32(Color.Green));
        ExtTextOut(hPrinterDC, rc.left, rc.top, ETO_OPAQUE, IntPtr.Zero, sbText, (uint)sbText.Length, IntPtr.Zero);
    
        var nExtra = -10.0f / nScaleX;
        //SetTextCharacterExtra(hPrinterDC, MulDiv(nExtra, 1440, nLogPixelsXPrinter));
        SetTextCharacterExtra(hPrinterDC, (int)nExtra);
        rc.top += -lf.lfHeight;
        SetTextColor(hPrinterDC, ColorTranslator.ToWin32(Color.Red));
        ExtTextOut(hPrinterDC, rc.left, rc.top, ETO_OPAQUE, IntPtr.Zero, sbText, (uint)sbText.Length, IntPtr.Zero);
    
        SelectObject(hPrinterDC, hFontOld);
        DeleteObject(hFontNew);
    }

            [DllImport("Gdi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
            public static extern bool SetWorldTransform(IntPtr hdc, ref XFORM lpxf);
    
            [StructLayout(LayoutKind.Sequential)]
            public struct XFORM
            {
                public float eM11;
                public float eM12;
                public float eM21;
                public float eM22;
                public float eDx;
                public float eDy;
            }
    
            [DllImport("Gdi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
            public static extern int SetGraphicsMode(IntPtr hdc, int iMode);
    
            public const int GM_COMPATIBLE = 1;
            public const int GM_ADVANCED = 2;
            public const int GM_LAST = 2;
            
            [DllImport("User32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern IntPtr GetDC(IntPtr hWnd);
    
            [DllImport("User32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
            
            public const int TRANSPARENT = 1;
            public const int OPAQUE = 2;
            public const int BKMODE_LAST = 2;
    
            [DllImport("Gdi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
            public static extern int SetBkMode(IntPtr hdc, int mode);


    Saturday, June 6, 2020 1:18 PM
  • I tried to adapt your code and use SetWorldTransform to keep same x,y for GDI and GDI+, but it doesn't work (blue text (GDI) should be at same position as black text (GDI+))...
    And the fonts have not exactly the same size

    Test code =>

    var inputString = "This is drawn by ";
    var fontSize = 31f;
    //var font = new Font("Avenir", fontSize, new FontStyle());
    // var font = new Font("Times New Roman", fontSize, new FontStyle());
    var font = new Font("Avenir Black", fontSize, new FontStyle());
    
    var startX1 = 50;
    var startY1 = 50;
    // Draw string by Graphics
    e.Graphics.DrawString(inputString + "DrawString", font, new SolidBrush(Color.Black), startX1, startY1);
    
    // Draw string by ExtTextOut
    LOGFONT lf = new LOGFONT();
    font.ToLogFont(lf, e.Graphics);
    
    IntPtr hPrinterDC = e.Graphics.GetHdc();
    StringBuilder sbText = new StringBuilder(inputString + "ExtTextOut");
    if (hPrinterDC != IntPtr.Zero)
    {
        int nPrintHorzRes = GetDeviceCaps(hPrinterDC, HORZRES);
        int nPrintVertRes = GetDeviceCaps(hPrinterDC, VERTRES);
        int nPhysWidth = GetDeviceCaps(hPrinterDC, PHYSICALWIDTH);
        int nPhysHeight = GetDeviceCaps(hPrinterDC, PHYSICALHEIGHT);
        int nPhysOffsetX = GetDeviceCaps(hPrinterDC, PHYSICALOFFSETX);
        int nPhysOffsetY = GetDeviceCaps(hPrinterDC, PHYSICALOFFSETY);
    
        IntPtr hDCScreen = GetDC(IntPtr.Zero);
        float nLogPixelsXScreen = GetDeviceCaps(hDCScreen, LOGPIXELSX);
        float nLogPixelsYScreen = GetDeviceCaps(hDCScreen, LOGPIXELSY);
        ReleaseDC(hDCScreen, IntPtr.Zero);
        float nLogPixelsXPrinter = GetDeviceCaps(hPrinterDC, LOGPIXELSX);
        float nLogPixelsYPrinter = GetDeviceCaps(hPrinterDC, LOGPIXELSY);
    
        float nScaleX = Math.Max(nLogPixelsXScreen, nLogPixelsXPrinter) / Math.Min(nLogPixelsXScreen, nLogPixelsXPrinter);
        float nScaleY = Math.Max(nLogPixelsYScreen, nLogPixelsYPrinter) / Math.Min(nLogPixelsYScreen, nLogPixelsYPrinter);
    
        System.Drawing.Drawing2D.Matrix transform = new System.Drawing.Drawing2D.Matrix();
        transform.Scale(nScaleX, nScaleY);
        XFORM renderTransform = new XFORM();
        var elements = transform.Elements;
        var m11 = elements[0];
        var m12 = elements[1];
        var m21 = elements[2];
        var m22 = elements[3];
        var dx = elements[4];
        var dy = elements[5];
    
        renderTransform.eM11 = (float)m11;
        renderTransform.eM12 = (float)m12;
        renderTransform.eM21 = (float)m21;
        renderTransform.eM22 = (float)m22;
        // Test offsets for (50, 50) showing font sizes are different when they start at same positions
        // int nOffsetX = 34;
        // int nOffsetY = -14;
        int nOffsetX = 0;
        int nOffsetY = 0;
        renderTransform.eDx = (float)transform.OffsetX + nOffsetX;
        renderTransform.eDy = (float)transform.OffsetY + nOffsetY;
        SetGraphicsMode(hPrinterDC, GM_ADVANCED);
        //SetMapMode(hPrinterDC, MM_LOENGLISH);
        SetMapMode(hPrinterDC, MM_TEXT);
        bool bRet = SetWorldTransform(hPrinterDC, ref renderTransform);
    
        var startX2 = startX1;
        var startY2 = startY1;
        RECT rc = new RECT(startX2, startY2, nPhysWidth, nPhysHeight);
    
        //Rectangle margins = e.MarginBounds;
        //// inches => mm => pixels
        //int nLeftPixels = MulDiv(margins.Left, 2540, 100000);
        //nLeftPixels = MulDiv(nLeftPixels, (int)nLogPixelsXScreen, 72);
        //startX2 += nLeftPixels;
    
        //// inches => mm => pixels
        //int nTopPixels = MulDiv(margins.Top, 2540, 100000);
        //nTopPixels = MulDiv(nTopPixels, (int)nLogPixelsYScreen, 72);
        //startY2 += nTopPixels;
    
        //rc.left = startX2;
        //rc.top = startY2;
    
        SetViewportOrgEx(hPrinterDC, -nPhysOffsetX, -nPhysOffsetY, IntPtr.Zero);
    
        //lf.lfHeight = MulDiv((int)-font.Size, (int)nLogPixelsYPrinter, 72);
        lf.lfHeight = (int)(lf.lfHeight / nScaleY);   
    
        IntPtr hFontNew = CreateFontIndirect(lf);
        IntPtr hFontOld = SelectObject(hPrinterDC, hFontNew);
    
        SetBkMode(hPrinterDC, TRANSPARENT);
        SetTextColor(hPrinterDC, ColorTranslator.ToWin32(Color.Blue));      
        ExtTextOut(hPrinterDC, rc.left, rc.top, ETO_OPAQUE, IntPtr.Zero, sbText, (uint)sbText.Length, IntPtr.Zero);
    
        rc.top += -lf.lfHeight;
        SetTextColor(hPrinterDC, ColorTranslator.ToWin32(Color.Green));
        ExtTextOut(hPrinterDC, rc.left, rc.top, ETO_OPAQUE, IntPtr.Zero, sbText, (uint)sbText.Length, IntPtr.Zero);
    
        var nExtra = -10.0f / nScaleX;
        //SetTextCharacterExtra(hPrinterDC, MulDiv(nExtra, 1440, nLogPixelsXPrinter));
        SetTextCharacterExtra(hPrinterDC, (int)nExtra);
        rc.top += -lf.lfHeight;
        SetTextColor(hPrinterDC, ColorTranslator.ToWin32(Color.Red));
        ExtTextOut(hPrinterDC, rc.left, rc.top, ETO_OPAQUE, IntPtr.Zero, sbText, (uint)sbText.Length, IntPtr.Zero);
    
        SelectObject(hPrinterDC, hFontOld);
        DeleteObject(hFontNew);
    }

            [DllImport("Gdi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
            public static extern bool SetWorldTransform(IntPtr hdc, ref XFORM lpxf);
    
            [StructLayout(LayoutKind.Sequential)]
            public struct XFORM
            {
                public float eM11;
                public float eM12;
                public float eM21;
                public float eM22;
                public float eDx;
                public float eDy;
            }
    
            [DllImport("Gdi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
            public static extern int SetGraphicsMode(IntPtr hdc, int iMode);
    
            public const int GM_COMPATIBLE = 1;
            public const int GM_ADVANCED = 2;
            public const int GM_LAST = 2;
            
            [DllImport("User32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern IntPtr GetDC(IntPtr hWnd);
    
            [DllImport("User32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
            
            public const int TRANSPARENT = 1;
            public const int OPAQUE = 2;
            public const int BKMODE_LAST = 2;
    
            [DllImport("Gdi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
            public static extern int SetBkMode(IntPtr hdc, int mode);


    I got the expected output from this method. I needed to do some changes like:

    For Scaling, I used screen pixel to 100 default:
    float nScaleX = Math.Max(100, nLogPixelsXPrinter) / Math.Min(100, nLogPixelsXPrinter);
    float nScaleY = Math.Max(100, nLogPixelsYPrinter) / Math.Min(100, nLogPixelsYPrinter);

    And I set origin point to 0:
    SetViewportOrgEx(hPrinterDC, 0, 0, IntPtr.Zero);

    After doing these changes, I had some problems with horizontal/vertical alignments which I have calculated manually(using GetTextExtentPoint32A) and it worked. 

    Thanks for your help !!

    A.R.Prajapati



    A.R.Prajapati

    Friday, July 31, 2020 6:06 AM

All replies

  • This function works with GDI TextOut() and not GDI+ DrawString() that .NET Framework uses (you see it's from gdi32.dll instread of gdiplus.dll)

    Check here for various workarounds by drawing it character-by-character.

    EDIT: Since GDI is a bit old and Microsoft won't change it any more (in fact, GDI+ is also in similar situation. However since .NET Framework WinForm is build around it, using GDI+ based functions are kind of okay now)

    For modern graphics functions that allows you to adjust kerning (this is the proper typography term for what you're trying to change), I can only find this article involving DirectWrite in Win8+ . You may want to take a look but I think this would be overkill if you only want this functionality because it don't have equivalent class in .NET Framework runtime and needs wrapper to use.

    Friday, May 29, 2020 3:06 AM
    Answerer
  • Thanks for your reply.

    I have gone through this article and I want to achieve the same so I think I need to create a wrapper and use it in C# application.


    A.R.Prajapati

    Friday, May 29, 2020 4:59 AM
  • It works fine with DirectWrite (a lot of P/Invoke...) =>

    Friday, May 29, 2020 6:34 AM
  • Try this code too:

    public void Draw( Graphics g )
    {
    	IntPtr hdc = g.GetHdc( );
    	SetTextCharacterExtra( hdc, 24 );
    
    	using( var g2 = Graphics.FromHdc( hdc ) )
    	{
    		TextRenderer.DrawText( g2, "str", Font, new Point( 0, 0 ), Color.Black );
    		g.ReleaseHdc( hdc );
    	}
    }
    

    Friday, May 29, 2020 8:10 AM
  • Yeap, I tried it and it text character spacing is working using TextRenderer as well but text printed using TextRenderer is very smaller in size as compared to Graphics.DrawString. I am not sure why. Please see this output of TextRenderer.DrawText vs Graphics.DrawString with same inputs(fonts, location):  

    

    A.R.Prajapati

    Monday, June 1, 2020 1:32 AM
  • It works fine with DirectWrite (a lot of P/Invoke...) =>

    Yeap Castorix31, I want to implement exactly same as this. Would you please share me code blocks(C++) if possible as I need to create C++ wrapper to use it in my C# code. I have gone through microsoft document for this and I am not clear how to pass current Graphics context(HDC) so this will change the character spacing. My main question is, can I pass HDC(e.Graphics.GetGdc()) to DirectWrite function(SetCharacterSpacing) which will change the spacing? Will it work like that way or It will only work when we draw text using TextWrite functions?

    A.R.Prajapati


    Monday, June 1, 2020 1:47 AM
  • Please refer to explanation of Michael Kaplan, ex-Microsoft employee who is responsible for i18n related subjects.

    The quoted KB article is removed from support.microsoft.com when it clean materials related to WinXP, but Wayback Machine backs you up.

    EDIT: TL;DR version : GDI is written with assumption that everything you try to render is 100% (i.e.: unscaled) but GDI+ can handle scaling automatically. It's the scaling part that introduce this size difference.

    You may do the calculation on adjustment yourself using information outlined here if you want.

    Monday, June 1, 2020 2:31 AM
    Answerer
  • Hey cheong00, I have gone through the links you provided for explanation of GDI vs GDI+ and I have just done the font adjustments as well i.e. for TextRenderer I am using font size by (fontSize * 20) / 15 but still the fonts are very very smaller than one drawn using Graphics.DrawString.

    A.R.Prajapati

    Monday, June 1, 2020 3:08 AM
  • Don't multiply it with fixed value, always adjust it with Graphics.DpiX and DpiY instead. (The article said the value comes from Twips already)

    That's why I don't recommand people use GDI functions now.

    Monday, June 1, 2020 4:16 AM
    Answerer
  • Okay, can you please explain me how to calculate font size based on DpiX and Y. Any reference link will be enough. Thanks

    A.R.Prajapati

    Monday, June 1, 2020 6:23 AM
  • Unfortunately I can't find any reference that I used 15+ years ago, and I certainly no longer have access to the code I've written at that time.

    Since I don't have development environment to try to reconstruct it now, other helpers please feel free to help.

    It's related to the fact that screen DPI was always assumed to be 72 DPI back in WinXP or before.

    Monday, June 1, 2020 7:39 AM
    Answerer
  • Yeap Castorix31, I want to implement exactly same as this. Would you please share me code blocks(C++) if possible as I need to create C++ wrapper to use it in my C# code.


    I tested DirectWrite directly in C#, with P/Invoke
    But as you said that you wanted to print, it is more complex with DirectWrite and I tested only on screen...

    So, a simpler print test with GDI and ExtTextOut =>

    StringBuilder sbText = new StringBuilder("This is a test");
    IntPtr hPrinterDC = GetPrinterDC();
    if (hPrinterDC != IntPtr.Zero)
    {
        int nPrintHorzRes = GetDeviceCaps(hPrinterDC, HORZRES);
        int nPrintVertRes = GetDeviceCaps(hPrinterDC, VERTRES);
        int nPrintLogPixelsX = GetDeviceCaps(hPrinterDC, LOGPIXELSX);
        int nPrintLogPixelsY = GetDeviceCaps(hPrinterDC, LOGPIXELSY);
        int nPhysWidth = GetDeviceCaps(hPrinterDC, PHYSICALWIDTH);
        int nPhysHeight = GetDeviceCaps(hPrinterDC, PHYSICALHEIGHT);
        int nPhysOffsetX = GetDeviceCaps(hPrinterDC, PHYSICALOFFSETX);
        int nPhysOffsetY = GetDeviceCaps(hPrinterDC, PHYSICALOFFSETY);
    
        DOCINFO di = new DOCINFO();
        di.cbSize = Marshal.SizeOf(typeof(DOCINFO));
        di.lpszDocName = "Test GDI";
        if (StartDoc(hPrinterDC, di) > 0)
        {
            if (StartPage(hPrinterDC) > 0)
            {
                RECT rc = new RECT(0, 0, nPhysWidth, nPhysHeight);
                SetMapMode(hPrinterDC, MM_TEXT);
                SetViewportOrgEx(hPrinterDC, -nPhysOffsetX, -nPhysOffsetY, IntPtr.Zero);
    
                LOGFONT lf = new LOGFONT();
                lf.lfHeight = -20 * nPrintLogPixelsY / 72;
                lf.lfWeight = FW_NORMAL;
                lf.lfCharSet = DEFAULT_CHARSET;
                lf.lfFaceName = "Arial";
                IntPtr hFontNew = CreateFontIndirect(lf);
                IntPtr hFontOld = SelectObject(hPrinterDC, hFontNew);
    
                SetTextColor(hPrinterDC, ColorTranslator.ToWin32(Color.Blue));
                ExtTextOut(hPrinterDC, rc.left, rc.top, ETO_OPAQUE, IntPtr.Zero, sbText, (uint)sbText.Length, IntPtr.Zero);
    
                SetTextCharacterExtra(hPrinterDC, MulDiv(20, 1440, nPrintLogPixelsX));
                rc.top += -lf.lfHeight;                      
                SetTextColor(hPrinterDC, ColorTranslator.ToWin32(Color.Red));
                ExtTextOut(hPrinterDC, rc.left, rc.top, ETO_OPAQUE, IntPtr.Zero, sbText, (uint)sbText.Length, IntPtr.Zero);
    
                SelectObject(hPrinterDC, hFontOld);
                DeleteObject(hFontNew);
                EndPage(hPrinterDC);
            }
            EndDoc(hPrinterDC);
        }
        else
        {
            int nError = Marshal.GetLastWin32Error();
            System.Windows.Forms.MessageBox.Show("Error : " + nError.ToString(), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
        DeleteDC(hPrinterDC);
    }

    Declarations :

            [StructLayout(LayoutKind.Sequential)]
            public struct RECT
            {
                public int left;
                public int top;
                public int right;
                public int bottom;
                public RECT(int Left, int Top, int Right, int Bottom)
                {
                    left = Left;
                    top = Top;
                    right = Right;
                    bottom = Bottom;
                }
            }
    
            [DllImport("User32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern IntPtr GetDC(IntPtr hWnd);
    
            [DllImport("User32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern IntPtr GetWindowDC(IntPtr hWnd);
    
            [DllImport("User32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
    
            [DllImport("Gdi32.dll", SetLastError = true)]
            public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiObj);
    
            [DllImport("Gdi32.dll", SetLastError = true)]
            public static extern bool DeleteObject([In] IntPtr hObject);
    
            [DllImport("Gdi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern bool DeleteDC(IntPtr hdc);
    
            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
            public class DOCINFO
            {
                public int cbSize = 20;
                public string lpszDocName;
                public string lpszOutput;
                public string lpszDatatype;
                public int fwType;
            }
    
            [DllImport("Gdi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern int StartDoc(IntPtr hDC, DOCINFO lpDocInfo);
    
            [DllImport("Gdi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern int StartPage(IntPtr hDC);
    
            [DllImport("Gdi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern int EndPage(IntPtr hDC);
    
            [DllImport("Gdi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern int AbortDoc(IntPtr hDC);
    
            [DllImport("Gdi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern int EndDoc(IntPtr hDC);
    
            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
            public class PRINTDLG
            {
                public int lStructSize;
                public IntPtr hwndOwner;
                public IntPtr hDevMode;
                public IntPtr hDevNames;
                public IntPtr hDC;
                public int Flags;
                public short nFromPage;
                public short nToPage;
                public short nMinPage;
                public short nMaxPage;
                public short nCopies;
                public IntPtr hInstance;
                public IntPtr lCustData;
                public IntPtr lpfnPrintHook;
                public IntPtr lpfnSetupHook;
                public string lpPrintTemplateName;
                public string lpSetupTemplateName;
                public IntPtr hPrintTemplate;
                public IntPtr hSetupTemplate;
            }
    
            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 1)]
            public class PRINTDLGX86
            {
                public int lStructSize;
                public IntPtr hwndOwner;
                public IntPtr hDevMode;
                public IntPtr hDevNames;
                public IntPtr hDC;
                public int Flags;
                public short nFromPage;
                public short nToPage;
                public short nMinPage;
                public short nMaxPage;
                public short nCopies;
                public IntPtr hInstance;
                public IntPtr lCustData;
                public IntPtr lpfnPrintHook;
                public IntPtr lpfnSetupHook;
                public string lpPrintTemplateName;
                public string lpSetupTemplateName;
                public IntPtr hPrintTemplate;
                public IntPtr hSetupTemplate;
            }
    
            public const int PD_RETURNDC = 0x00000100;
            public const int PD_RETURNDEFAULT = 0x00000400;
    
            [DllImport("Comdlg32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern bool PrintDlg([In, Out] PRINTDLG lppd);
    
            [DllImport("Comdlg32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern bool PrintDlg([In, Out] PRINTDLGX86 lppd);
    
            public IntPtr GetPrinterDC()
            {
                PRINTDLGX86 pdlg = new PRINTDLGX86();
                pdlg.lStructSize = Marshal.SizeOf(typeof(PRINTDLGX86));
                //pdlg.Flags = PD_RETURNDEFAULT | PD_RETURNDC;
                pdlg.Flags = PD_RETURNDC;
                PrintDlg(pdlg);
                return pdlg.hDC;
            }
    
            public const int HORZRES = 8; /* Horizontal width in pixels */
            public const int VERTRES = 10; /* Vertical height in pixels */
            public const int PHYSICALWIDTH = 110; /* Physical Width in device units */
            public const int PHYSICALHEIGHT = 111; /* Physical Height in device units */
            public const int PHYSICALOFFSETX = 112; /* Physical Printable Area x margin */
            public const int PHYSICALOFFSETY = 113; /* Physical Printable Area y margin */
            public const int LOGPIXELSX = 88;   /* Logical pixels/inch in X */
            public const int LOGPIXELSY = 90;   /* Logical pixels/inch in Y */
    
            [DllImport("Gdi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
    
            [DllImport("Gdi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern bool SetViewportOrgEx(IntPtr hdc, int x, int y, IntPtr lppt);
    
            [DllImport("Gdi32.dll", SetLastError = true)]
            public static extern IntPtr CreateFontIndirect([In, MarshalAs(UnmanagedType.LPStruct)] LOGFONT lplf);
    
            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
            public class LOGFONT
            {
                public int lfHeight = 0;
                public int lfWidth = 0;
                public int lfEscapement = 0;
                public int lfOrientation = 0;
                public int lfWeight = 0;
                public byte lfItalic = 0;
                public byte lfUnderline = 0;
                public byte lfStrikeOut = 0;
                public byte lfCharSet = 0;
                public byte lfOutPrecision = 0;
                public byte lfClipPrecision = 0;
                public byte lfQuality = 0;
                public byte lfPitchAndFamily = 0;
                [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
                public string lfFaceName = string.Empty;
            }
    
            /* Font Weights */
            public const int FW_DONTCARE = 0;
            public const int FW_THIN = 100;
            public const int FW_EXTRALIGHT = 200;
            public const int FW_LIGHT = 300;
            public const int FW_NORMAL = 400;
            public const int FW_MEDIUM = 500;
            public const int FW_SEMIBOLD = 600;
            public const int FW_BOLD = 700;
            public const int FW_EXTRABOLD = 800;
            public const int FW_HEAVY = 900;
    
            public const int ANSI_CHARSET = 0;
            public const int DEFAULT_CHARSET = 1;
            public const int SYMBOL_CHARSET = 2;
    
            [DllImport("Gdi32.dll", SetLastError = true)]
            public static extern int SetTextCharacterExtra(IntPtr hdc, int extra);
    
            [DllImport("Kernel32.dll", SetLastError = true)]
            public static extern int MulDiv(int nNumber, int nNumerator, int nDenominator);
    
            [DllImport("Gdi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
            public static extern bool ExtTextOut(IntPtr hdc, int x, int y, uint options, IntPtr lprect, StringBuilder lpString, uint c, IntPtr lpDx);
    
            public const int ETO_OPAQUE = 0x0002;
            public const int ETO_CLIPPED = 0x0004;
            public const int ETO_GLYPH_INDEX = 0x0010;
            public const int ETO_RTLREADING = 0x0080;
            public const int ETO_NUMERICSLOCAL = 0x0400;
            public const int ETO_NUMERICSLATIN = 0x0800;
            public const int ETO_IGNORELANGUAGE = 0x1000;
            public const int ETO_PDY = 0x2000;
            public const int ETO_REVERSE_INDEX_MAP = 0x10000;
    
            [DllImport("Gdi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern int SetMapMode(IntPtr hdc, int iMode);
    
            public const int MM_TEXT = 1;
            public const int MM_LOMETRIC = 2;
            public const int MM_HIMETRIC = 3;
            public const int MM_LOENGLISH = 4;
            public const int MM_HIENGLISH = 5;
            public const int MM_TWIPS = 6;
            public const int MM_ISOTROPIC = 7;
            public const int MM_ANISOTROPIC = 8;
    
            [DllImport("Gdi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
            public static extern int SetTextColor(IntPtr hdc, int color);
    

    Monday, June 1, 2020 4:40 PM
  • Thanks Castorix31 for sharing code sample. Actually I am using System.Drawing and specifically Graphics.DrawString method everywhere which internally using gdiplus.dll. In above example, SetTextCharacterExtra is of Gdi32.dll. So it will not fit in my existing system. I thought DirectWrite is suitable as per my need. Do you have DirectWrite code sample? As I am not sure how can I pass Hdc object which I got from Graphics from PrintPage event. Here is my sample code block:

    public void Draw(Graphics g) { PrintDocument pd = new PrintDocument(); pd.PrintPage += new PrintPageEventHandler(delegate (Object sender, PrintPageEventArgs e) {

    // Tried this but not working //IntPtr hdc = e.Graphics.GetHdc(); //SetTextCharacterExtra(hdc, 24); //e.Graphics.ReleaseHdc(hdc);

    e.Graphics.DrawString("This is sample string.",this.Font,Brushes.Black,0,0); } }




    A.R.Prajapati


    Monday, June 1, 2020 11:30 PM
  • In above example, SetTextCharacterExtra is of Gdi32.dll. So it will not fit in my existing system.


    But this is the API you tried to use in your original message...

    As cheong00 said, this API only works with functions which use GDI, like TextRenderer, and of course ExtTextOut

    I have shown in the sample how to scale the text

    I used full GDI, but you can simply copy the code in PrintPageEventHandler event,

    where hPrinterDC = e.Graphics.GetHdc();

    and add for example a PrintPreviewDialog  =>



    • Edited by Castorix31 Tuesday, June 2, 2020 7:42 AM
    Tuesday, June 2, 2020 7:35 AM
  • I'll add that his best route is probably to "Check here for various workarounds by drawing it character-by-character."

    In this way you can forget the problem about GDI scaling here, and you only use whatever is provided by .NET framework hence your code will not need special treatment when porting to .NET 5 (.NET Core 3.1+)

    Tuesday, June 2, 2020 10:57 AM
    Answerer
  • Yeap Castorix31, I was using this API(SetTextCharacterExtra) in original message and it was not working - that is my original issue. As I am  using Graphics.DrawString(GDI+), this API(of GDI) is not working.

    I have tested your previous code sample(ExtTextout) as it is working perfectly. If I follow this approach, I need to rewrite my code to use GDI everywhere. Till now, I have only one problem with ExtTextOut - it is using LOGFONT and I have System.Drawing.Font and when I use font.ToLogFont() - I got LOGFONT but font weight is not proper. See the output of ExtTextOut and Graphics.DrawString with same inputs(fonts, positions):


    Moreover, I am using Graphics.MeasureString to get the estimated size for particular string. Based on it, I change the position of texts. How to do it if I use ExtTextout?

    I have tried TextRendered as well but I am having some problem of font size so it didn't fit well in my existing app.


    A.R.Prajapati



    Tuesday, June 2, 2020 11:52 PM
  • Yeap cheong00, I have gone through it initially but It is not working as perfect as I need. The main problem with printing individual character is, some character has uneven spacing which result in uneven space between characters in whole word. This is what I got when I drew individual character. Please see space between f and a in Infant word.

     

    A.R.Prajapati

    Tuesday, June 2, 2020 11:59 PM
  • I fixed the font size with Muldiv
    I added a test with DrawString (in black) by using the same font and I get the same size as with ExtTextOut

    The PrintPageEventHandler event  =>

    private void printDocument1_PrintPage(System.Object sender, System.Drawing.Printing.PrintPageEventArgs e)
    {
        IntPtr hPrinterDC = e.Graphics.GetHdc();
        Rectangle margins = e.MarginBounds;
    
        StringBuilder sbText = new StringBuilder("This is a test");
        float nFontSize = 20.0f;
    
        int nPrintHorzRes = GetDeviceCaps(hPrinterDC, HORZRES);
        int nPrintVertRes = GetDeviceCaps(hPrinterDC, VERTRES);
        int nPrintLogPixelsX = GetDeviceCaps(hPrinterDC, LOGPIXELSX);
        int nPrintLogPixelsY = GetDeviceCaps(hPrinterDC, LOGPIXELSY);
        int nPhysWidth = GetDeviceCaps(hPrinterDC, PHYSICALWIDTH);
        int nPhysHeight = GetDeviceCaps(hPrinterDC, PHYSICALHEIGHT);
        int nPhysOffsetX = GetDeviceCaps(hPrinterDC, PHYSICALOFFSETX);
        int nPhysOffsetY = GetDeviceCaps(hPrinterDC, PHYSICALOFFSETY);            
    
        RECT rc = new RECT(0, 0, nPhysWidth, nPhysHeight);
        int nLeftPixels = MulDiv(margins.Left, 2540, 100000);
        nLeftPixels = nLeftPixels * nPrintLogPixelsX / 72;
        int nTopPixels = MulDiv(margins.Top, 2540, 100000);
        nTopPixels = nTopPixels * nPrintLogPixelsY / 72;
        rc.left += (int)nLeftPixels;
        rc.left += 2; // +2 difference ?
        rc.top += nTopPixels;
        SetMapMode(hPrinterDC, MM_TEXT);
        SetViewportOrgEx(hPrinterDC, -nPhysOffsetX, -nPhysOffsetY, IntPtr.Zero);
    
        LOGFONT lf = new LOGFONT();
        //lf.lfHeight = (int)(-nFontSize * nPrintLogPixelsY / 72);
        lf.lfHeight = MulDiv((int)-nFontSize, nPrintLogPixelsY, 72);
        lf.lfWeight = FW_NORMAL;
        lf.lfCharSet = DEFAULT_CHARSET;
        lf.lfFaceName = "Arial";
        IntPtr hFontNew = CreateFontIndirect(lf);
        IntPtr hFontOld = SelectObject(hPrinterDC, hFontNew);
    
        SetTextColor(hPrinterDC, ColorTranslator.ToWin32(Color.Blue));
        ExtTextOut(hPrinterDC, rc.left, rc.top, ETO_OPAQUE, IntPtr.Zero, sbText, (uint)sbText.Length, IntPtr.Zero);
    
        SetTextCharacterExtra(hPrinterDC, MulDiv(20, 1440, nPrintLogPixelsX));
        rc.top += -lf.lfHeight;
        SetTextColor(hPrinterDC, ColorTranslator.ToWin32(Color.Red));
        ExtTextOut(hPrinterDC, rc.left, rc.top, ETO_OPAQUE, IntPtr.Zero, sbText, (uint)sbText.Length, IntPtr.Zero);
    
        e.Graphics.ReleaseHdc(hPrinterDC);
    
        using (Font font = new Font("Arial", nFontSize))
        {              
            e.Graphics.DrawString(sbText.ToString(), font, System.Drawing.Brushes.Black, new PointF(0, 10));
        }
    }

    Wednesday, June 3, 2020 12:09 PM
  • Hey Castorix31,

    I have used this MulDiv to set font height but output is still the same. The problem is with font weight. I am using Avenir Black which is Bold fonts by default. Here is my code block. Here I have put fonts static but actually it will be configured on screen by end user. 

    		private static void PrintByExtTextout(Graphics g, string input, Color textColor, Rectangle rect)
    		{
    			var font = new Font("Avenir Black", 31, new FontStyle(), GraphicsUnit.Point);
    			LOGFONT lf = new LOGFONT();
    			font.ToLogFont(lf, g);
    
    			IntPtr hPrinterDC = g.GetHdc();
    			StringBuilder sbText = new StringBuilder(input);
    
    			int nPrintLogPixelsY = GetDeviceCaps(hPrinterDC, LOGPIXELSY);
    			lf.lfHeight = MulDiv((int)-font.Size, nPrintLogPixelsY, 72);
    
    			if (hPrinterDC != IntPtr.Zero)
    			{
    				RECT rc = new RECT(rect);
    				SetMapMode(hPrinterDC, MM_TEXT);
    				IntPtr hFontNew = CreateFontIndirect(lf);
    				IntPtr hFontOld = SelectObject(hPrinterDC, hFontNew);
    				var nExtra = -8;
    				SetTextCharacterExtra(hPrinterDC, nExtra);
    				rc.top += -lf.lfHeight;
    				SetTextColor(hPrinterDC, ColorTranslator.ToWin32(textColor));
    				ExtTextOut(hPrinterDC, rc.left, rc.top, ETO_OPAQUE, IntPtr.Zero, sbText, (uint)sbText.Length, IntPtr.Zero);
    
    				SelectObject(hPrinterDC, hFontOld);
    				DeleteObject(hFontNew);
    			}
    			g.ReleaseHdc();
    		}

    And here is the output:

     


    And one more problem with ExtTextOut is, how can I set the X and Y position of the drawn text? I have RectangleF which I have been passing to e.Graphics.DrawString. If you check above output, I am passing coordinates X = 50 to both functions but starting position is different for ExtTextOut output. I want to replace e.Graphics.DrawString with ExtTextOut but only when it works like before.

    Here is one more test output for starting position X = 200:



    I think both functions are using coordinates in different units. Graphics unit is Display and for ExtTextOut, we set MapMode to MM_Text - which may equal to pixel. Now how can I manipulate coordinates to pass in ExtTextOut so it will work like same as Graphics.DrawString? Any idea? 

    A.R.Prajapati




    Wednesday, June 3, 2020 11:52 PM
  • I forgot the Charset in the declaration of CreateFontIndirect

    [DllImport("Gdi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern IntPtr CreateFontIndirect([In, MarshalAs(UnmanagedType.LPStruct)] LOGFONT lplf);

    I tested with an "Avenir" font  I downloaded from Google and it still works for me

    For ExtTexOut coordinates, they must be converted with LOGPIXELSX, LOGPIXELSY

    But there is also the Margin, 100 by default (inches) which must be converted into pixels
    You can see I did a conversion in my test, but the margin is not exact (I have a few pixels of difference if I set 0,0 for example, to be fixed)

    Thursday, June 4, 2020 9:55 AM
  • Thanks Castorix31 for your response. That font related difference is now fixed by declaring Charset.

    I have tried to convert coordinates for ExtTextOut but It seems It does not as same as Graphics.DrawString does. I think I am doing something wrong while converting it. What I understand is, I need to convert value from graphics unit Display(unit of measure of Graphics.DrawString) to Pixel(unit of measure of ExtTextOut). As display is 1/100 inch and 1 inch = 96 pixels so based on this method, I converted coordinates for ExtTextOut but still it is not same as DrawString output. Here is my full code block which I am using and the output:

    		static void Main(string[] args)
    		{
    			PrintDocument pd = new PrintDocument();
    			pd.PrintPage += new PrintPageEventHandler(delegate (Object sender, PrintPageEventArgs e)
    			{
    				/*
    				 The problem statement is, horizontal starting point is different when drawstring using Graphics.DrawString and ExtTextOut
    				 I am passing same horizontal starting position. When I pass startX = 0, both are same but not when I pass 50, 100 or more. Run below code by changing startX value only.
    				 This is certainly because unit of measure of Graphics.DrawString(Display) is different than that of ExtTextOut(I set it to MM_TEXT)
    			     When I set the Graphics PageUnit to Pixel, both are staring from same position but I cannot do it as whole application will get disturbed.
    				 
    				 TODO: I need to convert this startX to relevant value for ExtTextOut
    			    */
    				//e.Graphics.PageUnit = GraphicsUnit.Pixel;		 This works but I cannot do it. Read above note please.
    
    				var startX = 50;
    				var startY1 = 50;               // vertical start point of Graphics.DrawString
    				var startY2 = 300;              // vertical start point of ExtTextOut
    
    				var inputString = "This is drawn by ";
    				var fontSize = 31f;
    				var font = new Font("Avenir Black", fontSize, new FontStyle());
    
    
    				// DrawString  by Graphics
    				e.Graphics.DrawString(inputString + "DrawString", font, new SolidBrush(Color.Black), startX, startY1);
    
    
    				// Draw string by ExtTextOut
    				LOGFONT lf = new LOGFONT();
    				font.ToLogFont(lf, e.Graphics);
    
    				IntPtr hPrinterDC = e.Graphics.GetHdc();
    				StringBuilder sbText = new StringBuilder(inputString + "ExtTextOut");
    				if (hPrinterDC != IntPtr.Zero)
    				{
    					int nPrintHorzRes = GetDeviceCaps(hPrinterDC, HORZRES);
    					int nPrintVertRes = GetDeviceCaps(hPrinterDC, VERTRES);
    					int nPrintLogPixelsX = GetDeviceCaps(hPrinterDC, LOGPIXELSX);
    					int nPrintLogPixelsY = GetDeviceCaps(hPrinterDC, LOGPIXELSY);
    					int nPhysWidth = GetDeviceCaps(hPrinterDC, PHYSICALWIDTH);
    					int nPhysHeight = GetDeviceCaps(hPrinterDC, PHYSICALHEIGHT);
    					int nPhysOffsetX = GetDeviceCaps(hPrinterDC, PHYSICALOFFSETX);
    					int nPhysOffsetY = GetDeviceCaps(hPrinterDC, PHYSICALOFFSETY);
    
    					RECT rc = new RECT(startX, startY2, nPhysWidth, nPhysHeight);
    
    					// TODO - need to convert X to pixel by considering Display = 1/100 inch and 1 inch = 96 pixel
    					// Not sure if this correct or not?
    					var convertStartX = MulDiv(rc.left, 96, 100);
    					convertStartX = convertStartX * nPrintLogPixelsX / 72;
    					rc.left = convertStartX;
    
    					//margin
    					int nLeftPixels = MulDiv(e.MarginBounds.Left, 2540, 100000);
    					nLeftPixels = nLeftPixels * nPrintLogPixelsX / 72;
    
    					int nTopPixels = MulDiv(e.MarginBounds.Top, 2540, 100000);
    					nTopPixels = nTopPixels * nPrintLogPixelsY / 72;
    
    					rc.left += (int)nLeftPixels;
    					rc.left += 10; // +2 difference ?		- had to increase diff to make it equaivalant to Drawstring start point
    
    					rc.top += nTopPixels;
    
    					SetMapMode(hPrinterDC, MM_TEXT);
    					SetViewportOrgEx(hPrinterDC, -nPhysOffsetX, -nPhysOffsetY, IntPtr.Zero);
    
    					lf.lfHeight = MulDiv((int)-font.Size, nPrintLogPixelsY, 72);
    					IntPtr hFontNew = CreateFontIndirect(lf);
    					IntPtr hFontOld = SelectObject(hPrinterDC, hFontNew);
    
    					var nExtra = -10;
    					SetTextCharacterExtra(hPrinterDC, nExtra);
    					rc.top += -lf.lfHeight;
    					SetTextColor(hPrinterDC, ColorTranslator.ToWin32(Color.Black));
    					ExtTextOut(hPrinterDC, rc.left, rc.top, ETO_OPAQUE, IntPtr.Zero, sbText, (uint)sbText.Length, IntPtr.Zero);
    
    					SelectObject(hPrinterDC, hFontOld);
    					DeleteObject(hFontNew);
    				}
    				e.Graphics.ReleaseHdc();
    			});
    			pd.Print();
    		}

    And here is the output:

    Castorix31, thank you once again for helping me so far.


    A.R.Prajapati

    Friday, June 5, 2020 1:54 AM
  • I tried to adapt your code and use SetWorldTransform to keep same x,y for GDI and GDI+, but it doesn't work (blue text (GDI) should be at same position as black text (GDI+))...
    And the fonts have not exactly the same size

    Test code =>

    var inputString = "This is drawn by ";
    var fontSize = 31f;
    //var font = new Font("Avenir", fontSize, new FontStyle());
    // var font = new Font("Times New Roman", fontSize, new FontStyle());
    var font = new Font("Avenir Black", fontSize, new FontStyle());
    
    var startX1 = 50;
    var startY1 = 50;
    // Draw string by Graphics
    e.Graphics.DrawString(inputString + "DrawString", font, new SolidBrush(Color.Black), startX1, startY1);
    
    // Draw string by ExtTextOut
    LOGFONT lf = new LOGFONT();
    font.ToLogFont(lf, e.Graphics);
    
    IntPtr hPrinterDC = e.Graphics.GetHdc();
    StringBuilder sbText = new StringBuilder(inputString + "ExtTextOut");
    if (hPrinterDC != IntPtr.Zero)
    {
        int nPrintHorzRes = GetDeviceCaps(hPrinterDC, HORZRES);
        int nPrintVertRes = GetDeviceCaps(hPrinterDC, VERTRES);
        int nPhysWidth = GetDeviceCaps(hPrinterDC, PHYSICALWIDTH);
        int nPhysHeight = GetDeviceCaps(hPrinterDC, PHYSICALHEIGHT);
        int nPhysOffsetX = GetDeviceCaps(hPrinterDC, PHYSICALOFFSETX);
        int nPhysOffsetY = GetDeviceCaps(hPrinterDC, PHYSICALOFFSETY);
    
        IntPtr hDCScreen = GetDC(IntPtr.Zero);
        float nLogPixelsXScreen = GetDeviceCaps(hDCScreen, LOGPIXELSX);
        float nLogPixelsYScreen = GetDeviceCaps(hDCScreen, LOGPIXELSY);
        ReleaseDC(hDCScreen, IntPtr.Zero);
        float nLogPixelsXPrinter = GetDeviceCaps(hPrinterDC, LOGPIXELSX);
        float nLogPixelsYPrinter = GetDeviceCaps(hPrinterDC, LOGPIXELSY);
    
        float nScaleX = Math.Max(nLogPixelsXScreen, nLogPixelsXPrinter) / Math.Min(nLogPixelsXScreen, nLogPixelsXPrinter);
        float nScaleY = Math.Max(nLogPixelsYScreen, nLogPixelsYPrinter) / Math.Min(nLogPixelsYScreen, nLogPixelsYPrinter);
    
        System.Drawing.Drawing2D.Matrix transform = new System.Drawing.Drawing2D.Matrix();
        transform.Scale(nScaleX, nScaleY);
        XFORM renderTransform = new XFORM();
        var elements = transform.Elements;
        var m11 = elements[0];
        var m12 = elements[1];
        var m21 = elements[2];
        var m22 = elements[3];
        var dx = elements[4];
        var dy = elements[5];
    
        renderTransform.eM11 = (float)m11;
        renderTransform.eM12 = (float)m12;
        renderTransform.eM21 = (float)m21;
        renderTransform.eM22 = (float)m22;
        // Test offsets for (50, 50) showing font sizes are different when they start at same positions
        // int nOffsetX = 34;
        // int nOffsetY = -14;
        int nOffsetX = 0;
        int nOffsetY = 0;
        renderTransform.eDx = (float)transform.OffsetX + nOffsetX;
        renderTransform.eDy = (float)transform.OffsetY + nOffsetY;
        SetGraphicsMode(hPrinterDC, GM_ADVANCED);
        //SetMapMode(hPrinterDC, MM_LOENGLISH);
        SetMapMode(hPrinterDC, MM_TEXT);
        bool bRet = SetWorldTransform(hPrinterDC, ref renderTransform);
    
        var startX2 = startX1;
        var startY2 = startY1;
        RECT rc = new RECT(startX2, startY2, nPhysWidth, nPhysHeight);
    
        //Rectangle margins = e.MarginBounds;
        //// inches => mm => pixels
        //int nLeftPixels = MulDiv(margins.Left, 2540, 100000);
        //nLeftPixels = MulDiv(nLeftPixels, (int)nLogPixelsXScreen, 72);
        //startX2 += nLeftPixels;
    
        //// inches => mm => pixels
        //int nTopPixels = MulDiv(margins.Top, 2540, 100000);
        //nTopPixels = MulDiv(nTopPixels, (int)nLogPixelsYScreen, 72);
        //startY2 += nTopPixels;
    
        //rc.left = startX2;
        //rc.top = startY2;
    
        SetViewportOrgEx(hPrinterDC, -nPhysOffsetX, -nPhysOffsetY, IntPtr.Zero);
    
        //lf.lfHeight = MulDiv((int)-font.Size, (int)nLogPixelsYPrinter, 72);
        lf.lfHeight = (int)(lf.lfHeight / nScaleY);   
    
        IntPtr hFontNew = CreateFontIndirect(lf);
        IntPtr hFontOld = SelectObject(hPrinterDC, hFontNew);
    
        SetBkMode(hPrinterDC, TRANSPARENT);
        SetTextColor(hPrinterDC, ColorTranslator.ToWin32(Color.Blue));      
        ExtTextOut(hPrinterDC, rc.left, rc.top, ETO_OPAQUE, IntPtr.Zero, sbText, (uint)sbText.Length, IntPtr.Zero);
    
        rc.top += -lf.lfHeight;
        SetTextColor(hPrinterDC, ColorTranslator.ToWin32(Color.Green));
        ExtTextOut(hPrinterDC, rc.left, rc.top, ETO_OPAQUE, IntPtr.Zero, sbText, (uint)sbText.Length, IntPtr.Zero);
    
        var nExtra = -10.0f / nScaleX;
        //SetTextCharacterExtra(hPrinterDC, MulDiv(nExtra, 1440, nLogPixelsXPrinter));
        SetTextCharacterExtra(hPrinterDC, (int)nExtra);
        rc.top += -lf.lfHeight;
        SetTextColor(hPrinterDC, ColorTranslator.ToWin32(Color.Red));
        ExtTextOut(hPrinterDC, rc.left, rc.top, ETO_OPAQUE, IntPtr.Zero, sbText, (uint)sbText.Length, IntPtr.Zero);
    
        SelectObject(hPrinterDC, hFontOld);
        DeleteObject(hFontNew);
    }

            [DllImport("Gdi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
            public static extern bool SetWorldTransform(IntPtr hdc, ref XFORM lpxf);
    
            [StructLayout(LayoutKind.Sequential)]
            public struct XFORM
            {
                public float eM11;
                public float eM12;
                public float eM21;
                public float eM22;
                public float eDx;
                public float eDy;
            }
    
            [DllImport("Gdi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
            public static extern int SetGraphicsMode(IntPtr hdc, int iMode);
    
            public const int GM_COMPATIBLE = 1;
            public const int GM_ADVANCED = 2;
            public const int GM_LAST = 2;
            
            [DllImport("User32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern IntPtr GetDC(IntPtr hWnd);
    
            [DllImport("User32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
            
            public const int TRANSPARENT = 1;
            public const int OPAQUE = 2;
            public const int BKMODE_LAST = 2;
    
            [DllImport("Gdi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
            public static extern int SetBkMode(IntPtr hdc, int mode);


    Saturday, June 6, 2020 1:18 PM
  • Thanks for your response. I have used this latest solution(SetWorldTransform) in my actual project.

    With these transformation changes, please see the output I am getting from my actual project vs the output from Graphics.DrawString: Output is little close to DrawString but there is still some problem with X and Y coordinates. Do you know how to fix it? 

    As a another way, do you think we can do something to convert X and Y for ExtTextOut which should be equivalent to Graphics.DrawString uses internally? It will be okay if we have difference of 3-4 pixels. In Code version before SetWorldTransform, can we convert X and Y coordinates like this or any other method?

    // TODO - need to convert X to pixel by considering Display = 1/100 inch and 1 inch = 96 pixel
    // Not sure if this correct or not?
    var convertStartX = MulDiv(rc.left, 96, 100);
    convertStartX = convertStartX * nPrintLogPixelsX / 72;
    rc.left = convertStartX;


    A.R.Prajapati


    Tuesday, June 9, 2020 2:09 AM
  • Hello Castorix31,

    Do you know any way that I can convert GDI+ coordinates to GDI so above design will work?  

    Thanks


    A.R.Prajapati


    Sunday, June 14, 2020 11:20 PM
  • Hi,

    Please see if the content in this link is helpful to you.

    About GDI/GDI+ coordinate compatibility?

    Best Regards,

    Timon


    MSDN Community Support
    Please remember to click "Mark as Answer" the responses that resolved your issue, and to click "Unmark as Answer" if not. This can be beneficial to other community members reading this thread. If you have any compliments or complaints to MSDN Support, feel free to contact MSDNFSF@microsoft.com.

    Tuesday, June 16, 2020 5:39 AM
  • I tried to adapt your code and use SetWorldTransform to keep same x,y for GDI and GDI+, but it doesn't work (blue text (GDI) should be at same position as black text (GDI+))...
    And the fonts have not exactly the same size

    Test code =>

    var inputString = "This is drawn by ";
    var fontSize = 31f;
    //var font = new Font("Avenir", fontSize, new FontStyle());
    // var font = new Font("Times New Roman", fontSize, new FontStyle());
    var font = new Font("Avenir Black", fontSize, new FontStyle());
    
    var startX1 = 50;
    var startY1 = 50;
    // Draw string by Graphics
    e.Graphics.DrawString(inputString + "DrawString", font, new SolidBrush(Color.Black), startX1, startY1);
    
    // Draw string by ExtTextOut
    LOGFONT lf = new LOGFONT();
    font.ToLogFont(lf, e.Graphics);
    
    IntPtr hPrinterDC = e.Graphics.GetHdc();
    StringBuilder sbText = new StringBuilder(inputString + "ExtTextOut");
    if (hPrinterDC != IntPtr.Zero)
    {
        int nPrintHorzRes = GetDeviceCaps(hPrinterDC, HORZRES);
        int nPrintVertRes = GetDeviceCaps(hPrinterDC, VERTRES);
        int nPhysWidth = GetDeviceCaps(hPrinterDC, PHYSICALWIDTH);
        int nPhysHeight = GetDeviceCaps(hPrinterDC, PHYSICALHEIGHT);
        int nPhysOffsetX = GetDeviceCaps(hPrinterDC, PHYSICALOFFSETX);
        int nPhysOffsetY = GetDeviceCaps(hPrinterDC, PHYSICALOFFSETY);
    
        IntPtr hDCScreen = GetDC(IntPtr.Zero);
        float nLogPixelsXScreen = GetDeviceCaps(hDCScreen, LOGPIXELSX);
        float nLogPixelsYScreen = GetDeviceCaps(hDCScreen, LOGPIXELSY);
        ReleaseDC(hDCScreen, IntPtr.Zero);
        float nLogPixelsXPrinter = GetDeviceCaps(hPrinterDC, LOGPIXELSX);
        float nLogPixelsYPrinter = GetDeviceCaps(hPrinterDC, LOGPIXELSY);
    
        float nScaleX = Math.Max(nLogPixelsXScreen, nLogPixelsXPrinter) / Math.Min(nLogPixelsXScreen, nLogPixelsXPrinter);
        float nScaleY = Math.Max(nLogPixelsYScreen, nLogPixelsYPrinter) / Math.Min(nLogPixelsYScreen, nLogPixelsYPrinter);
    
        System.Drawing.Drawing2D.Matrix transform = new System.Drawing.Drawing2D.Matrix();
        transform.Scale(nScaleX, nScaleY);
        XFORM renderTransform = new XFORM();
        var elements = transform.Elements;
        var m11 = elements[0];
        var m12 = elements[1];
        var m21 = elements[2];
        var m22 = elements[3];
        var dx = elements[4];
        var dy = elements[5];
    
        renderTransform.eM11 = (float)m11;
        renderTransform.eM12 = (float)m12;
        renderTransform.eM21 = (float)m21;
        renderTransform.eM22 = (float)m22;
        // Test offsets for (50, 50) showing font sizes are different when they start at same positions
        // int nOffsetX = 34;
        // int nOffsetY = -14;
        int nOffsetX = 0;
        int nOffsetY = 0;
        renderTransform.eDx = (float)transform.OffsetX + nOffsetX;
        renderTransform.eDy = (float)transform.OffsetY + nOffsetY;
        SetGraphicsMode(hPrinterDC, GM_ADVANCED);
        //SetMapMode(hPrinterDC, MM_LOENGLISH);
        SetMapMode(hPrinterDC, MM_TEXT);
        bool bRet = SetWorldTransform(hPrinterDC, ref renderTransform);
    
        var startX2 = startX1;
        var startY2 = startY1;
        RECT rc = new RECT(startX2, startY2, nPhysWidth, nPhysHeight);
    
        //Rectangle margins = e.MarginBounds;
        //// inches => mm => pixels
        //int nLeftPixels = MulDiv(margins.Left, 2540, 100000);
        //nLeftPixels = MulDiv(nLeftPixels, (int)nLogPixelsXScreen, 72);
        //startX2 += nLeftPixels;
    
        //// inches => mm => pixels
        //int nTopPixels = MulDiv(margins.Top, 2540, 100000);
        //nTopPixels = MulDiv(nTopPixels, (int)nLogPixelsYScreen, 72);
        //startY2 += nTopPixels;
    
        //rc.left = startX2;
        //rc.top = startY2;
    
        SetViewportOrgEx(hPrinterDC, -nPhysOffsetX, -nPhysOffsetY, IntPtr.Zero);
    
        //lf.lfHeight = MulDiv((int)-font.Size, (int)nLogPixelsYPrinter, 72);
        lf.lfHeight = (int)(lf.lfHeight / nScaleY);   
    
        IntPtr hFontNew = CreateFontIndirect(lf);
        IntPtr hFontOld = SelectObject(hPrinterDC, hFontNew);
    
        SetBkMode(hPrinterDC, TRANSPARENT);
        SetTextColor(hPrinterDC, ColorTranslator.ToWin32(Color.Blue));      
        ExtTextOut(hPrinterDC, rc.left, rc.top, ETO_OPAQUE, IntPtr.Zero, sbText, (uint)sbText.Length, IntPtr.Zero);
    
        rc.top += -lf.lfHeight;
        SetTextColor(hPrinterDC, ColorTranslator.ToWin32(Color.Green));
        ExtTextOut(hPrinterDC, rc.left, rc.top, ETO_OPAQUE, IntPtr.Zero, sbText, (uint)sbText.Length, IntPtr.Zero);
    
        var nExtra = -10.0f / nScaleX;
        //SetTextCharacterExtra(hPrinterDC, MulDiv(nExtra, 1440, nLogPixelsXPrinter));
        SetTextCharacterExtra(hPrinterDC, (int)nExtra);
        rc.top += -lf.lfHeight;
        SetTextColor(hPrinterDC, ColorTranslator.ToWin32(Color.Red));
        ExtTextOut(hPrinterDC, rc.left, rc.top, ETO_OPAQUE, IntPtr.Zero, sbText, (uint)sbText.Length, IntPtr.Zero);
    
        SelectObject(hPrinterDC, hFontOld);
        DeleteObject(hFontNew);
    }

            [DllImport("Gdi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
            public static extern bool SetWorldTransform(IntPtr hdc, ref XFORM lpxf);
    
            [StructLayout(LayoutKind.Sequential)]
            public struct XFORM
            {
                public float eM11;
                public float eM12;
                public float eM21;
                public float eM22;
                public float eDx;
                public float eDy;
            }
    
            [DllImport("Gdi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
            public static extern int SetGraphicsMode(IntPtr hdc, int iMode);
    
            public const int GM_COMPATIBLE = 1;
            public const int GM_ADVANCED = 2;
            public const int GM_LAST = 2;
            
            [DllImport("User32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern IntPtr GetDC(IntPtr hWnd);
    
            [DllImport("User32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
            
            public const int TRANSPARENT = 1;
            public const int OPAQUE = 2;
            public const int BKMODE_LAST = 2;
    
            [DllImport("Gdi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
            public static extern int SetBkMode(IntPtr hdc, int mode);


    I got the expected output from this method. I needed to do some changes like:

    For Scaling, I used screen pixel to 100 default:
    float nScaleX = Math.Max(100, nLogPixelsXPrinter) / Math.Min(100, nLogPixelsXPrinter);
    float nScaleY = Math.Max(100, nLogPixelsYPrinter) / Math.Min(100, nLogPixelsYPrinter);

    And I set origin point to 0:
    SetViewportOrgEx(hPrinterDC, 0, 0, IntPtr.Zero);

    After doing these changes, I had some problems with horizontal/vertical alignments which I have calculated manually(using GetTextExtentPoint32A) and it worked. 

    Thanks for your help !!

    A.R.Prajapati



    A.R.Prajapati

    Friday, July 31, 2020 6:06 AM