Skip to content

Instantly share code, notes, and snippets.

@ArthurHub
Last active May 25, 2023 16:38
Show Gist options
  • Save ArthurHub/10527752 to your computer and use it in GitHub Desktop.
Save ArthurHub/10527752 to your computer and use it in GitHub Desktop.
/// <summary>
/// Wrapper for GDI text rendering functions<br/>
/// This class is not thread-safe as GDI function should be called from the UI thread.
/// </summary>
/// <remarks>
/// http://theartofdev.com/2013/08/12/using-native-gdi-for-text-rendering-in-c/<br/>
/// The MIT License (MIT) Copyright (c) 2014 Arthur Teplitzki.
/// </remarks>
public sealed class NativeTextRenderer : IDisposable
{
#region Fields and Consts
/// <summary>
/// used for <see cref="MeasureString(string,System.Drawing.Font,float,out int,out int)"/> calculation.
/// </summary>
private static readonly int[] _charFit = new int[1];
/// <summary>
/// used for <see cref="MeasureString(string,System.Drawing.Font,float,out int,out int)"/> calculation.
/// </summary>
private static readonly int[] _charFitWidth = new int[1000];
/// <summary>
/// cache of all the font used not to create same font again and again
/// </summary>
private static readonly Dictionary<string, Dictionary<float, Dictionary<FontStyle, IntPtr>>> _fontsCache = new Dictionary<string, Dictionary<float, Dictionary<FontStyle, IntPtr>>>(StringComparer.InvariantCultureIgnoreCase);
/// <summary>
/// The wrapped WinForms graphics object
/// </summary>
private readonly Graphics _g;
/// <summary>
/// the initialized HDC used
/// </summary>
private IntPtr _hdc;
#endregion
/// <summary>
/// Init.
/// </summary>
public NativeTextRenderer(Graphics g)
{
_g = g;
var clip = _g.Clip.GetHrgn(_g);
_hdc = _g.GetHdc();
SetBkMode(_hdc, 1);
SelectClipRgn(_hdc, clip);
DeleteObject(clip);
}
/// <summary>
/// Measure the width and height of string <paramref name="str"/> when drawn on device context HDC
/// using the given font <paramref name="font"/>.
/// </summary>
/// <param name="str">the string to measure</param>
/// <param name="font">the font to measure string with</param>
/// <returns>the size of the string</returns>
public Size MeasureString(string str, Font font)
{
SetFont(font);
var size = new Size();
GetTextExtentPoint32(_hdc, str, str.Length, ref size);
return size;
}
/// <summary>
/// Measure the width and height of string <paramref name="str"/> when drawn on device context HDC
/// using the given font <paramref name="font"/>.<br/>
/// Restrict the width of the string and get the number of characters able to fit in the restriction and
/// the width those characters take.
/// </summary>
/// <param name="str">the string to measure</param>
/// <param name="font">the font to measure string with</param>
/// <param name="maxWidth">the max width to render the string in</param>
/// <param name="charFit">the number of characters that will fit under <see cref="maxWidth"/> restriction</param>
/// <param name="charFitWidth"></param>
/// <returns>the size of the string</returns>
public Size MeasureString(string str, Font font, float maxWidth, out int charFit, out int charFitWidth)
{
SetFont(font);
var size = new Size();
GetTextExtentExPoint(_hdc, str, str.Length, (int)Math.Round(maxWidth), _charFit, _charFitWidth, ref size);
charFit = _charFit[0];
charFitWidth = charFit > 0 ? _charFitWidth[charFit - 1] : 0;
return size;
}
/// <summary>
/// Draw the given string using the given font and foreground color at given location.
/// </summary>
/// <param name="str">the string to draw</param>
/// <param name="font">the font to use to draw the string</param>
/// <param name="color">the text color to set</param>
/// <param name="point">the location to start string draw (top-left)</param>
public void DrawString(String str, Font font, Color color, Point point)
{
SetFont(font);
SetTextColor(color);
TextOut(_hdc, point.X, point.Y, str, str.Length);
}
/// <summary>
/// Draw the given string using the given font and foreground color at given location.<br/>
/// See http://msdn.microsoft.com/en-us/library/windows/desktop/dd162498(v=vs.85).aspx.
/// </summary>
/// <param name="str">the string to draw</param>
/// <param name="font">the font to use to draw the string</param>
/// <param name="color">the text color to set</param>
/// <param name="rect">the rectangle in which the text is to be formatted</param>
/// <param name="flags">The method of formatting the text</param>
public void DrawString(String str, Font font, Color color, Rectangle rect, TextFormatFlags flags)
{
SetFont(font);
SetTextColor(color);
var rect2 = new Rect(rect);
DrawText(_hdc, str, str.Length, ref rect2, (uint)flags);
}
/// <summary>
/// Special draw logic to draw transparent text using GDI.<br/>
/// 1. Create in-memory DC<br/>
/// 2. Copy background to in-memory DC<br/>
/// 3. Draw the text to in-memory DC<br/>
/// 4. Copy the in-memory DC to the proper location with alpha blend<br/>
/// </summary>
public void DrawTransparentText(string str, Font font, Color color, Point point, Size size)
{
// Create a memory DC so we can work off-screen
IntPtr memoryHdc = CreateCompatibleDC(_hdc);
SetBkMode(memoryHdc, 1);
// Create a device-independent bitmap and select it into our DC
var info = new BitMapInfo();
info.biSize = Marshal.SizeOf(info);
info.biWidth = size.Width;
info.biHeight = -size.Height;
info.biPlanes = 1;
info.biBitCount = 32;
info.biCompression = 0; // BI_RGB
IntPtr ppvBits;
IntPtr dib = CreateDIBSection(_hdc, ref info, 0, out ppvBits, IntPtr.Zero, 0);
SelectObject(memoryHdc, dib);
try
{
// copy target background to memory HDC so when copied back it will have the proper background
BitBlt(memoryHdc, 0, 0, size.Width, size.Height, _hdc, point.X, point.Y, 0x00CC0020);
// Create and select font
SelectObject(memoryHdc, GetCachedHFont(font));
SetTextColor(memoryHdc, (color.B & 0xFF) << 16 | (color.G & 0xFF) << 8 | color.R);
// Draw text to memory HDC
TextOut(memoryHdc, 0, 0, str, str.Length);
// copy from memory HDC to normal HDC with alpha blend so achieve the transparent text
AlphaBlend(_hdc, point.X, point.Y, size.Width, size.Height, memoryHdc, 0, 0, size.Width, size.Height, new BlendFunction(color.A));
}
finally
{
DeleteObject(dib);
DeleteDC(memoryHdc);
}
}
/// <summary>
/// Release current HDC to be able to use <see cref="Graphics"/> methods.
/// </summary>
public void Dispose()
{
if (_hdc != IntPtr.Zero)
{
SelectClipRgn(_hdc, IntPtr.Zero);
_g.ReleaseHdc(_hdc);
_hdc = IntPtr.Zero;
}
}
#region Private methods
/// <summary>
/// Set a resource (e.g. a font) for the specified device context.
/// </summary>
private void SetFont(Font font)
{
SelectObject(_hdc, GetCachedHFont(font));
}
/// <summary>
/// Get cached unmanaged font handle for given font.<br/>
/// </summary>
/// <param name="font">the font to get unmanaged font handle for</param>
/// <returns>handle to unmanaged font</returns>
private static IntPtr GetCachedHFont(Font font)
{
IntPtr hfont = IntPtr.Zero;
Dictionary<float, Dictionary<FontStyle, IntPtr>> dic1;
if (_fontsCache.TryGetValue(font.Name, out dic1))
{
Dictionary<FontStyle, IntPtr> dic2;
if (dic1.TryGetValue(font.Size, out dic2))
{
dic2.TryGetValue(font.Style, out hfont);
}
else
{
dic1[font.Size] = new Dictionary<FontStyle, IntPtr>();
}
}
else
{
_fontsCache[font.Name] = new Dictionary<float, Dictionary<FontStyle, IntPtr>>();
_fontsCache[font.Name][font.Size] = new Dictionary<FontStyle, IntPtr>();
}
if (hfont == IntPtr.Zero)
{
_fontsCache[font.Name][font.Size][font.Style] = hfont = font.ToHfont();
}
return hfont;
}
/// <summary>
/// Set the text color of the device context.
/// </summary>
private void SetTextColor(Color color)
{
int rgb = (color.B & 0xFF) << 16 | (color.G & 0xFF) << 8 | color.R;
SetTextColor(_hdc, rgb);
}
[DllImport("gdi32.dll")]
private static extern int SetBkMode(IntPtr hdc, int mode);
[DllImport("gdi32.dll")]
private static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiObj);
[DllImport("gdi32.dll")]
private static extern int SetTextColor(IntPtr hdc, int color);
[DllImport("gdi32.dll", EntryPoint = "GetTextExtentPoint32W")]
private static extern int GetTextExtentPoint32(IntPtr hdc, [MarshalAs(UnmanagedType.LPWStr)] string str, int len, ref Size size);
[DllImport("gdi32.dll", EntryPoint = "GetTextExtentExPointW")]
private static extern bool GetTextExtentExPoint(IntPtr hDc, [MarshalAs(UnmanagedType.LPWStr)]string str, int nLength, int nMaxExtent, int[] lpnFit, int[] alpDx, ref Size size);
[DllImport("gdi32.dll", EntryPoint = "TextOutW")]
private static extern bool TextOut(IntPtr hdc, int x, int y, [MarshalAs(UnmanagedType.LPWStr)] string str, int len);
[DllImport("user32.dll", EntryPoint = "DrawTextW")]
private static extern int DrawText(IntPtr hdc, [MarshalAs(UnmanagedType.LPWStr)] string str, int len, ref Rect rect, uint uFormat);
[DllImport("gdi32.dll")]
private static extern int SelectClipRgn(IntPtr hdc, IntPtr hrgn);
[DllImport("gdi32.dll")]
private static extern bool DeleteObject(IntPtr hObject);
[DllImport("gdi32.dll", ExactSpelling = true, SetLastError = true)]
public static extern bool DeleteDC(IntPtr hdc);
[DllImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool BitBlt(IntPtr hdc, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, uint dwRop);
[DllImport("gdi32.dll", EntryPoint = "GdiAlphaBlend")]
private static extern bool AlphaBlend(IntPtr hdcDest, int nXOriginDest, int nYOriginDest, int nWidthDest, int nHeightDest, IntPtr hdcSrc, int nXOriginSrc, int nYOriginSrc, int nWidthSrc, int nHeightSrc, BlendFunction blendFunction);
[DllImport("gdi32.dll", ExactSpelling = true, SetLastError = true)]
private static extern IntPtr CreateCompatibleDC(IntPtr hdc);
[DllImport("gdi32.dll")]
private static extern IntPtr CreateDIBSection(IntPtr hdc, [In] ref BitMapInfo pbmi, uint iUsage, out IntPtr ppvBits, IntPtr hSection, uint dwOffset);
// ReSharper disable NotAccessedField.Local
// ReSharper disable MemberCanBePrivate.Local
// ReSharper disable FieldCanBeMadeReadOnly.Local
private struct Rect
{
private int _left;
private int _top;
private int _right;
private int _bottom;
public Rect(Rectangle r)
{
_left = r.Left;
_top = r.Top;
_bottom = r.Bottom;
_right = r.Right;
}
}
[StructLayout(LayoutKind.Sequential)]
private struct BlendFunction
{
public byte BlendOp;
public byte BlendFlags;
public byte SourceConstantAlpha;
public byte AlphaFormat;
public BlendFunction(byte alpha)
{
BlendOp = 0;
BlendFlags = 0;
AlphaFormat = 0;
SourceConstantAlpha = alpha;
}
}
[StructLayout(LayoutKind.Sequential)]
public struct BitMapInfo
{
public int biSize;
public int biWidth;
public int biHeight;
public short biPlanes;
public short biBitCount;
public int biCompression;
public int biSizeImage;
public int biXPelsPerMeter;
public int biYPelsPerMeter;
public int biClrUsed;
public int biClrImportant;
public byte bmiColors_rgbBlue;
public byte bmiColors_rgbGreen;
public byte bmiColors_rgbRed;
public byte bmiColors_rgbReserved;
}
#endregion
}
/// <summary>
/// See http://msdn.microsoft.com/en-us/library/windows/desktop/dd162498(v=vs.85).aspx
/// </summary>
[Flags]
public enum TextFormatFlags : uint
{
Default = 0x00000000,
Center = 0x00000001,
Right = 0x00000002,
VCenter = 0x00000004,
Bottom = 0x00000008,
WordBreak = 0x00000010,
SingleLine = 0x00000020,
ExpandTabs = 0x00000040,
TabStop = 0x00000080,
NoClip = 0x00000100,
ExternalLeading = 0x00000200,
CalcRect = 0x00000400,
NoPrefix = 0x00000800,
Internal = 0x00001000,
EditControl = 0x00002000,
PathEllipsis = 0x00004000,
EndEllipsis = 0x00008000,
ModifyString = 0x00010000,
RtlReading = 0x00020000,
WordEllipsis = 0x00040000,
NoFullWidthCharBreak = 0x00080000,
HidePrefix = 0x00100000,
ProfixOnly = 0x00200000,
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment