【问题标题】:How can I Use Win32 and GDI calls to write text characters to a bitmap如何使用 Win32 和 GDI 调用将文本字符写入位图
【发布时间】:2021-12-31 05:46:02
【问题描述】:

我正在开发一个使用 SharpGL 的 c# (.NET Framework) 程序。该库具有使用来自 Win32 的 wglUseFontBitmaps 调用将文本绘制到 OpenGL 上下文的方法;但是,该方法使用了在 v3.0 中弃用的显示列表。因此,我想找到一种使用 VBO 和 VAO 来绘制文本的方法。但是,即使字体大小为 10 或 12(我需要),wglUseFontBitmaps 方法也会生成相当清晰的文本。

我尝试使用多种方法来匹配该结果,包括 .NET 的 GlyphTypeface.GetGlyphOutlines 和 SharpFont(包装了 FreeType)。对于这两种字体,我尝试渲染更大尺寸的字体(没有抗锯齿)并让 OpenGL 将它们缩放到更小的尺寸。我仍然无法获得与 wglUseFontBitmaps 匹配的可靠且美观的结果。

所以,我目前的尝试是使用 Win32 GDI API 来编写文本,假设它可能会产生与 wglUseFontBitmaps 相似的结果;但是,我无法开始工作——只是将一个字符写入位图中。

下面我发布了一个完整的 c# 程序文件。它可以编译为 .NET Framework 控制台应用程序,但您必须添加对 System.Drawing 的引用,并且必须在“构建”选项卡下的项目首选项中打开“允许不安全代码”。

目前,它会创建非常奇怪的位图文件(顺便说一下,它会将名为“TMP.BMP”的测试文件写入您的桌面文件夹)。

这是代码——有点长,但包含了运行测试所需的所有内容:

using System;
using System.Drawing;
using System.Runtime.InteropServices;

namespace CharToBitmapConsoleTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var tester = new CharToBitmapTester();
            tester.RunTests();
        }

        // Calls the TestWithCharacter method a few times for testing
        public class CharToBitmapTester
        {
            public void RunTests()
            {
                var fontFamilyName = "Calibri";
                var fontHeight = 14;

                TestWithCharacter((int)'%', fontFamilyName, fontHeight);
                TestWithCharacter((int)'#', fontFamilyName, fontHeight);
                TestWithCharacter((int)'X', fontFamilyName, fontHeight);
                TestWithCharacter((int)'H', fontFamilyName, fontHeight);
            }


            /// <summary>
            /// Attempts to do every step needed to write a characte (corersponding to the given
            /// unicode index) into a bitmap using the given font family name and font height.
            /// The test returns true if any bits were written to memory as a result of the
            /// attempt. The test also writes a bitmap file (TMP.BMP) to the Users's desktop.
            /// </summary>
            /// <param name="unicodeIndex"></param>
            /// <param name="fontFamilyName"></param>
            /// <param name="fontHeight"></param>
            /// <returns></returns>
            public bool TestWithCharacter(int unicodeIndex, string fontFamilyName, int fontHeight)
            {
                //var hDC = gl.RenderContextProvider.DeviceContextHandle;
                //  Get the desktop DC.
                IntPtr desktopDC = WinGdi32.GetDC(IntPtr.Zero);
                //  Create our DC as a compatible DC for the desktop.
                var hDC = WinGdi32.CreateCompatibleDC(desktopDC);

                // Create the font handle (IntPtr) for the WinGDI font object
                var hFont = WinGdi32.CreateFont(fontHeight, 0, 0, 0, WinGdi32.FW_DONTCARE, 0, 0, 0, WinGdi32.DEFAULT_CHARSET,
                    WinGdi32.OUT_OUTLINE_PRECIS, WinGdi32.CLIP_DEFAULT_PRECIS, WinGdi32.CLEARTYPE_QUALITY, WinGdi32.VARIABLE_PITCH, fontFamilyName);

                // Select the font object into the Device Context
                // GDI actions will use hFont as the current font object
                WinGdi32.SelectObject(hDC, hFont);

                // Get the true widths for the glyph placement of all the characters
                var charWidthInfoArray = new WinGdi32.ABCFLOAT[256];
                WinGdi32.GetCharABCWidthsFloat(hDC, 0, 255, charWidthInfoArray);

                char character = (char)unicodeIndex;
                string characterAsString = character.ToString();

                var characterWidthInfo = charWidthInfoArray[unicodeIndex];
                var characterFullWidth = characterWidthInfo.abcfA + characterWidthInfo.abcfB + characterWidthInfo.abcfC;

                var glyphUnitWidth = (int)Math.Ceiling(characterWidthInfo.abcfB);
                var glyphUnitHeight = (int)fontHeight;


                //*************************************************************************************
                //  Create a DIBSection
                // 
                // Start with the BITMAPINFO
                var bitCount = 24;// 32;
                var info = new WinGdi32.BITMAPINFO();
                //  Set the data.
                info.biSize = Marshal.SizeOf(info);
                info.biBitCount = (short)bitCount;
                info.biPlanes = 1;
                info.biWidth = glyphUnitWidth;
                info.biHeight = glyphUnitHeight;

                IntPtr bits;
                //  Create the bitmap.
                var hBitmap = WinGdi32.CreateDIBSection(hDC, ref info, WinGdi32.DIB_RGB_COLORS, out bits, IntPtr.Zero, 0);
                WinGdi32.SelectObject(hDC, hBitmap);

                //  Set the pixel format.
                var pixelFormat = new WinGdi32.PIXELFORMATDESCRIPTOR();
                pixelFormat.Init();
                pixelFormat.nVersion = 1;
                pixelFormat.dwFlags = (WinGdi32.PFD_DRAW_TO_BITMAP | WinGdi32.PFD_SUPPORT_OPENGL | WinGdi32.PFD_SUPPORT_GDI);
                pixelFormat.iPixelType = WinGdi32.PFD_TYPE_RGBA;
                pixelFormat.cColorBits = (byte)bitCount;
                pixelFormat.cDepthBits = (byte)bitCount;
                pixelFormat.iLayerType = WinGdi32.PFD_MAIN_PLANE;

                //  Try to match a pixel format and note failure if we get an error
                int iPixelformat;
                if ((iPixelformat = WinGdi32.ChoosePixelFormat(hDC, pixelFormat)) == 0)
                    return false;

                //  Sets pixel format and test for errors
                if (WinGdi32.SetPixelFormat(hDC, iPixelformat, pixelFormat) == 0)
                {
                    //  Falure -- clear error and retur nfalse
                    int _ = Marshal.GetLastWin32Error();
                    return false;
                }

                //  Done Creating a DIBSection
                //  If I understand correctly, the hDC now has the DIBSction as the current object and
                //  calls related to drawing should go to it (and, I belive, fill our "bits" buffer)
                //*************************************************************************************


                // Set a location to output the text -- not really sure what to use here but going with 0, 0
                int x = 0;
                int y = 9;
                // Could play around with foreground and background colors...
                //var prevFgColorRef = WinGdi32.SetTextColor(hDC, ColorTranslator.ToWin32(System.Drawing.Color.White));
                //var prevBkColorRef = WinGdi32.SetBkColor(hDC, ColorTranslator.ToWin32(System.Drawing.Color.Black));
                // NOTE: we've already set hFont as the current font and hBitmap as the current bitmap...

                // Output the text -- this should go to the current bitmap and fill the bits buffer, right?
                var textOutWorked = WinGdi32.TextOut(hDC, x, y, characterAsString.ToString(), 1);

                if (textOutWorked)
                {
                    System.Diagnostics.Debug.WriteLine("TextOut finished without complaint");
                }
                else
                {
                    System.Diagnostics.Debug.WriteLine("TextOut says it did NOT work");
                    return false;
                }

                var dibSectionSize = glyphUnitWidth * glyphUnitHeight * bitCount;
                var testArray = new byte[dibSectionSize];
                Marshal.Copy(bits, testArray, 0, dibSectionSize);

                var bitsWithData = 0;
                foreach (var b in testArray)
                {
                    if (b != 0)
                    {
                        bitsWithData++;
                    }
                }
                System.Diagnostics.Debug.WriteLine(bitsWithData > 0 ? 
                    $"Test Wrote something to the bits! Font {fontFamilyName};  Character: {characterAsString}!" :
                    $"Test did NOT write to the bits! Font {fontFamilyName};  Character: {characterAsString}!");

                var stride = bitCount * glyphUnitWidth;
                using (Bitmap bitmap = new Bitmap(glyphUnitWidth, glyphUnitHeight, stride, System.Drawing.Imaging.PixelFormat.Format24bppRgb, bits))
                {
                    bitmap.Save(System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "TMP.BMP"));
                }

                return bitsWithData > 0;
            }

        }



        public static class WinGdi32
        {
            public const string Gdi32 = "gdi32.dll";
            public const string User32 = "user32.dll";

            /// <summary>
            /// The TextOut function writes a character string at the specified location, using the currently selected font, background color, and text color
            /// </summary>
            /// <param name="hDC">A handle to the device context.</param>
            /// <param name="x">The x-coordinate, in logical coordinates, of the reference point that the system uses to align the string.</param>
            /// <param name="y">The y-coordinate, in logical coordinates, of the reference point that the system uses to align the string.</param>
            /// <param name="str">The string to be drawn. The string does not need to be zero-terminated, because cchString specifies the length of the string.</param>
            /// <param name="c">The length of the string in characters.</param>
            /// <returns></returns>
            [DllImport(Gdi32, SetLastError = true)]
            public static extern bool TextOut(IntPtr hDC, int x, int y, [MarshalAs(UnmanagedType.LPStr)] string str, int c);


            /// <summary>
            /// The GetCharABCWidthsFloat function retrieves the widths, in logical units, of consecutive characters in a specified range from the current font.
            /// </summary>
            /// <param name="hDC">Handle to the device context.</param>
            /// <param name="iFirstChar">Specifies the code point of the first character in the group of consecutive characters where the ABC widths are seeked.</param>
            /// <param name="iLastChar">Specifies the code point of the last character in the group of consecutive characters where the ABC widths are seeked. This range is inclusive. An error is returned if the specified last character precedes the specified first character</param>
            /// <param name="ABCF">An array of ABCFLOAT structures that receives the character widths, in logical units</param>
            /// <returns></returns>
            [DllImport(Gdi32, SetLastError = true)]
            public static extern bool GetCharABCWidthsFloat(IntPtr hDC, uint iFirstChar, uint iLastChar, [Out, MarshalAs(UnmanagedType.LPArray)] ABCFLOAT[] ABCF);


            [DllImport(Gdi32, SetLastError = true)]
            public static extern IntPtr SetTextColor(IntPtr hDC, int crColor);
            [DllImport(Gdi32, SetLastError = true)]
            public static extern IntPtr SetBkColor(IntPtr hDC, int crColor);

            [DllImport(Gdi32, SetLastError = true)]
            public unsafe static extern int ChoosePixelFormat(IntPtr hDC, [In, MarshalAs(UnmanagedType.LPStruct)] PIXELFORMATDESCRIPTOR ppfd);

            [DllImport(Gdi32, SetLastError = true)]
            public static extern IntPtr CreateDIBSection(IntPtr hdc, [In] ref BITMAPINFO pbmi, uint pila, out IntPtr ppvBits, IntPtr hSection, uint dwOffset);

            [DllImport(Gdi32, SetLastError = true)]
            public unsafe static extern int SetPixelFormat(IntPtr hDC, int iPixelFormat, [In, MarshalAs(UnmanagedType.LPStruct)] PIXELFORMATDESCRIPTOR ppfd);


            [DllImport(User32, SetLastError = true)]
            public static extern IntPtr GetDC(IntPtr hWnd);

            [DllImport(Gdi32, SetLastError = true)]
            public static extern IntPtr CreateCompatibleDC(IntPtr hDC);

            [DllImport(Gdi32, SetLastError = true)]
            public static extern IntPtr CreateFont(int nHeight, int nWidth, int nEscapement,
                int nOrientation, uint fnWeight, uint fdwItalic, uint fdwUnderline, uint fdwStrikeOut,
                uint fdwCharSet, uint fdwOutputPrecision, uint fdwClipPrecision, uint fdwQuality,
                uint fdwPitchAndFamily, string lpszFace);

            [DllImport(Gdi32, SetLastError = true)]
            public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);







            /// <summary>
            /// The SIZE structure specifies the width and height of a rectangle.
            /// </summary>
            [StructLayout(LayoutKind.Sequential)]
            public class SIZE
            {
                /// <summary>
                /// Specifies the rectangle's width. The units depend on which function uses this.
                /// </summary>
                public long cx;
                /// <summary>
                /// Specifies the rectangle's height. The units depend on which function uses this.
                /// </summary>
                public long cy;
            }


            /// <summary>
            /// The ABCFLOAT structure contains the A, B, and C widths of a font character.
            /// </summary>
            public struct ABCFLOAT
            {
                /// <summary>
                /// The A spacing of the character.  The A spacing is the distance to add to the current position before drawing the character glyph.
                /// </summary>
                public float abcfA;
                /// <summary>
                /// The B spacing of the character.  The B spacing is the width of the drawn portion of the character glyph.
                /// </summary>
                public float abcfB;
                /// <summary>
                /// The C spacing of the character.  The C spacing is the distance to add to the current position to provide white space to the right of the character glyph.
                /// </summary>
                public float abcfC;
            }

            [StructLayout(LayoutKind.Sequential)]
            public struct BITMAPINFO
            {
                public Int32 biSize;
                public Int32 biWidth;
                public Int32 biHeight;
                public Int16 biPlanes;
                public Int16 biBitCount;
                public Int32 biCompression;
                public Int32 biSizeImage;
                public Int32 biXPelsPerMeter;
                public Int32 biYPelsPerMeter;
                public Int32 biClrUsed;
                public Int32 biClrImportant;

                public void Init()
                {
                    biSize = Marshal.SizeOf(this);
                }
            }


            [StructLayout(LayoutKind.Explicit)]
            public class PIXELFORMATDESCRIPTOR
            {
                [FieldOffset(0)]
                public UInt16 nSize;
                [FieldOffset(2)]
                public UInt16 nVersion;
                [FieldOffset(4)]
                public UInt32 dwFlags;
                [FieldOffset(8)]
                public Byte iPixelType;
                [FieldOffset(9)]
                public Byte cColorBits;
                [FieldOffset(10)]
                public Byte cRedBits;
                [FieldOffset(11)]
                public Byte cRedShift;
                [FieldOffset(12)]
                public Byte cGreenBits;
                [FieldOffset(13)]
                public Byte cGreenShift;
                [FieldOffset(14)]
                public Byte cBlueBits;
                [FieldOffset(15)]
                public Byte cBlueShift;
                [FieldOffset(16)]
                public Byte cAlphaBits;
                [FieldOffset(17)]
                public Byte cAlphaShift;
                [FieldOffset(18)]
                public Byte cAccumBits;
                [FieldOffset(19)]
                public Byte cAccumRedBits;
                [FieldOffset(20)]
                public Byte cAccumGreenBits;
                [FieldOffset(21)]
                public Byte cAccumBlueBits;
                [FieldOffset(22)]
                public Byte cAccumAlphaBits;
                [FieldOffset(23)]
                public Byte cDepthBits;
                [FieldOffset(24)]
                public Byte cStencilBits;
                [FieldOffset(25)]
                public Byte cAuxBuffers;
                [FieldOffset(26)]
                public SByte iLayerType;
                [FieldOffset(27)]
                public Byte bReserved;
                [FieldOffset(28)]
                public UInt32 dwLayerMask;
                [FieldOffset(32)]
                public UInt32 dwVisibleMask;
                [FieldOffset(36)]
                public UInt32 dwDamageMask;

                public void Init()
                {
                    nSize = (ushort)Marshal.SizeOf(this);
                }
            }

            public const uint FW_DONTCARE = 0;

            public const uint ANSI_CHARSET = 0;
            public const uint DEFAULT_CHARSET = 1;
            public const uint SYMBOL_CHARSET = 2;

            public const uint OUT_OUTLINE_PRECIS = 8;
            public const uint CLIP_DEFAULT_PRECIS = 0;

            public const uint CLEARTYPE_QUALITY = 5;

            public const uint FIXED_PITCH = 1;
            public const uint VARIABLE_PITCH = 2;

            public const uint DIB_RGB_COLORS = 0;

            public const uint PFD_DRAW_TO_BITMAP = 8;

            public const uint PFD_SUPPORT_GDI = 16;
            public const uint PFD_SUPPORT_OPENGL = 32;

            public const byte PFD_TYPE_RGBA = 0;

            public const sbyte PFD_MAIN_PLANE = 0;

        }

    }
}

如果有人能告诉我如何获取代码以将单个字符实际写入位图,我应该能够从那里获取并在我的 OpenGL 项目中使用它。

谢谢!

【问题讨论】:

  • 为什么不直接将 GDI+ 与官方 BitmapGraphics 类一起使用?
  • 为什么要调整位图文本的大小?如果您需要多个尺寸,只需为每种尺寸制作一个图集,它不会给您带来任何真正的麻烦。然后你可以使用适当的抗锯齿文本(虽然如果你想在 3D 中渲染,进行完整的亚像素渲染 ala ClearType 真的很棘手)。就像 Charlieface 说的那样,当你有 GDI+ 可用而不处理手动本机互操作时,使用 GDI 没有什么意义。如果您能展示您遇到的那种麻烦(例如,用于比较您得到的与您想要的)的屏幕截图,那将非常有帮助。
  • @Luaan,感谢您的评论。我通常会为每个字体系列和大小生成不同的位图;但是,我发现使用GetGlyphOutlines 和 SharpFont,较小的尺寸质量较差,因此我尝试了将它们放大并让 OpenGL 调整它们大小的方法(仅在某些情况下会稍微好一些)。我使用 GDI 是因为我更熟悉它的调用(SharpGL 使用它们),但我会研究 GDI+。我会考虑发布一些图片,虽然它们可以突出我的整体问题,但我发布的代码会生成几乎空的位图,这是我当前的问题。
  • @FTLPhysicsGuy 你应该先尝试更大的位图;如果有一些您不期望的偏移量等,我不会感到惊讶(通常文本大小函数会考虑重音之类的东西,即使它们不存在 - 否则重音字符将不适合)。跨度>

标签: c# bitmap gdi


【解决方案1】:

只是为了说明,这将是一个使用 GDI+ 的示例:

var text = "Hello world!";
var font = new Font("Calibri", 8);
var bitmap = new Bitmap(200, 100);
var targetRectangle = new RectangleF(0, 0, 200, 120);

var sf = new StringFormat(StringFormat.GenericDefault);
sf.SetMeasurableCharacterRanges(
      Enumerable.Range(0, text.Length)
      .Select(i => new CharacterRange(i, 1)).ToArray());

using (var gr = Graphics.FromImage(bitmap))
{
    gr.Clear(Color.Black);

    gr.DrawString(text, font, Brushes.White, targetRectangle, sf);

    var ranges = gr.MeasureCharacterRanges(text, font, targetRectangle, sf);   
    gr.DrawRectangles(Pens.Red, ranges.Select(i => i.GetBounds(gr)).ToArray());
}

bitmap.Dump();

这会生成一个包含文本“Hello world!”的位图。以及每个字符的区域(尽管这些实际上并不是字符的 bounds - 注意重音符号)。这允许您创建一个字符图集以便轻松渲染(当然,您希望字符之间有更大的间距来处理重叠)。

这会生成大约 6 emsize 的清晰文本(不过 8 更好),但这确实需要亚像素渲染,这意味着这仅适用于不需要缩放字体的情况(例如 UI )。有很多方法可以调整它 - 默认值是渲染速度和质量之间的折衷。例如,使用gr.PixelOffsetMode = PixelOffsetMode.Half; 将通过更好地对齐字符(以不那么“像素完美”为代价,为您提供更清晰、更漂亮的文本(尤其是对于没有针对小尺寸或最小锯齿进行优化的字体中的这种小字体) ”)。到 emsize 大约 12 时,即使稍微缩放(假设您正确配置了 ClearType),这些伪像也往往是不可见的。当然,结果取决于显示器像素的几何形状——它们不便于携带。 GDI 和 GDI+ 主要用于演示,而不是生成与设备无关的图形。您可能需要为此使用专门的软件。

您还可以使用gr.TextRenderingHint = TextRenderingHint.SingleBitPerPixel; 删除所有提示。这往往会导致现代字体的文本渲染效果不佳;它确实需要一种针对没有子像素的渲染进行优化的字体。主要优点是无论您的硬件如何,它看起来都一样 - 但它往往看起来很糟糕。

【讨论】:

  • 看起来很有希望,但我需要帮助才能将它融入我当前的构造。我正在使用 System.Windows.Media.GlyphTypeface 来了解恰好包含图集中每个字形中的像素的矩形。使用 GDI+,我如何知道每个字形中非空白像素的范围,以及如何知道将其放置在字符串中时向左/向下移动多少?例如,一个 16 磅的“!”字符宽 2 个像素,但 DrawString 在两侧添加像素以使其看起来“清晰”。如果像素大小与 GlyphTypeface 不匹配,则基线位置、方位或前进值也不匹配。你能帮忙吗?
  • @FTLPhysicsGuy 不幸的是,当您将矢量字体移动到光栅上时,您会得到这样的结果。根据 ! 的位置,它看起来会非常不同。 !一世!有两个不同的!。 GDI+ 的意思是现在在屏幕上显示东西。绘制文本是一个相当复杂的问题。在 DirectX 上,您可以使用 DirectWrite 来处理完美的实时文本渲染。您将永远从矢量字体缩减为位图获得完美的文本渲染 - 不要与您从例如您获得的文本进行比较。 DrawString,与使用位图字体的其他渲染相比。最好使用为此设计的字体。
  • @FTLPhysicsGuy 不幸的是,我认为 GDI+ 中没有任何(公共)函数可以帮助您进行字距调整和亚像素渲染。这一切都是在内部完成的,并且不可移植。如果您小心,将它与 GlyphTypeface 一起使用可以产生相当好的结果,但它不会 100% 起作用。 QuickFont 是与 OpenGL 一起使用的体面(且易于使用)的文本库,但它的质量无法与 DirectDraw 相提并论。当我还在处理游戏中的文字时,我总是选择手动处理位图字体(或者至少修饰光栅化)。
  • @FTLPhysicsGuy 对于 UI 文本,我更喜欢只渲染整段文本,而不是逐个字符地渲染,并根据需要缓存结果。这不是最快的方法,但对我来说从来都不是瓶颈(而且我一直在做大量 UI 和文本的游戏)。通过这种方式,您可以负担得起最好的文本渲染,这在处理现代字体和小字体时特别有用。它非常易于使用,为您提供对各种语言的全面支持,将所有负担转移到操作系统(或第 3 方库)...
  • @FTLPhysicsGuy 当然,如果你真的想要,你也可以只裁剪字符——这会给你一个不错的方法来使用DrawString 组合渲染(确保玩转渲染获得良好结果的选项)并从 GlyphTypeface(或 SharpFont)获取字距调整信息。它不会是完美的,它只会对英文文本产生相当好的效果,但它也很简单,文本缩放比例很好。
猜你喜欢
  • 1970-01-01
  • 2013-04-07
  • 2011-09-01
  • 2010-10-23
  • 1970-01-01
  • 2011-09-30
  • 2011-06-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多