Here's my version in C#:
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace WindowsFormsApp1
{
public static class WavyLineRendering
{
public static void Draw(Graphics graphics, Font font, Point ptTextPos, int nWidth, Color col)
{
// Draws a wavy underline below the position a run of text at the given
// position and of the given width would occupy. This method consults the
// height of the currently selected font in order to find the baseline where
// the underline is drawn.
// NOTE: The method will fail to find the correct position of the underline
// if the current text alignment is not set to TA_LEFT!
float fNumPixelsPerSample = 1.2f;
int nNumPts = (int)(nWidth / fNumPixelsPerSample);
if (nNumPts <= 1)
return;
// Retrieve information about the current GDI font.
var tm = GetTextMetricsWrapper(graphics, font);
// Create points array.
var arrPts = new PointF[nNumPts];
// Fill points array.
float fYOffset = 1.0f;
float fAmp = 1.5f;
for (int i = 0; i < nNumPts; i++)
{
arrPts[i].X = (float)(ptTextPos.X + (float)i * nWidth / (nNumPts - 1));
// The amplitude is computed as a function of the absolute position x rather
// than the sample index i in order to make sure the waveform will start at
// the correct point when two runs are drawn very near each-other.
float fValue = (float)(fAmp * Math.Sin((arrPts[i].X / fNumPixelsPerSample) * (Math.PI / 3.0)));
arrPts[i].Y = (float)(ptTextPos.Y + tm.tmAscent + tm.tmDescent * 0.5f + fYOffset + fValue);
}
// Draw the lines.
using (var pPen = new Pen(col))
{
graphics.SmoothingMode = SmoothingMode.HighQuality;
graphics.DrawLines(pPen, arrPts);
}
}
private static TEXTMETRIC GetTextMetricsWrapper(Graphics graphics, Font font)
{
var hDC = new HandleRef(graphics, graphics.GetHdc());
var hFont = new HandleRef(font, font.ToHfont());
try
{
var hFontPreviouse = SelectObject(hDC, hFont);
GetTextMetrics(hDC, out var textMetric);
SelectObject(hDC, hFontPreviouse);
return textMetric;
}
finally
{
DeleteObject(hFont);
graphics.ReleaseHdc(hDC.Handle);
}
}
[DllImport("Gdi32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SelectObject(HandleRef hdc, IntPtr hgdiobj);
[DllImport("Gdi32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SelectObject(HandleRef hdc, HandleRef hgdiobj);
[DllImport("Gdi32.dll", CharSet = CharSet.Auto)]
private static extern bool GetTextMetrics(HandleRef hdc, out TEXTMETRIC lptm);
[DllImport("Gdi32.dll", CharSet = CharSet.Auto)]
private static extern bool DeleteObject(HandleRef hdc);
[Serializable, StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
private struct TEXTMETRIC
{
public int tmHeight;
public int tmAscent;
public int tmDescent;
public int tmInternalLeading;
public int tmExternalLeading;
public int tmAveCharWidth;
public int tmMaxCharWidth;
public int tmWeight;
public int tmOverhang;
public int tmDigitizedAspectX;
public int tmDigitizedAspectY;
public byte tmFirstChar; // this assumes the ANSI charset; for the UNICODE charset the type is char (or short)
public byte tmLastChar; // this assumes the ANSI charset; for the UNICODE charset the type is char (or short)
public byte tmDefaultChar; // this assumes the ANSI charset; for the UNICODE charset the type is char (or short)
public byte tmBreakChar; // this assumes the ANSI charset; for the UNICODE charset the type is char (or short)
public byte tmItalic;
public byte tmUnderlined;
public byte tmStruckOut;
public byte tmPitchAndFamily;
public byte tmCharSet;
}
}
}