Как рисовать волнистые подчеркивания с Win32 TextOut
Я работаю над фрагментом кода, который в основном рисует текст на экране несколькими вызовами TextOut. Это приложение C32 Win32, я использую Visual Studio 2012. Как рисовать волнистые подчеркивания так, как это делает Word при маркировке орфографических ошибок и т. Д.?
TEXTMETRIC имеет элемент с именем tmUnderlined, который работает для обычного подчеркнутого текста, но не для волнистых подчеркиваний. Я также обнаружил, что элемент управления RichEdit от Microsoft поддерживает волнистые подчеркивания, но я тоже не могу его использовать.
Я мог бы, конечно, создать путь, используя простую синусоидальную волну, но прежде чем я это сделаю, я хотел проверить, есть ли более стандартный способ сделать это.
3 ответа
Я реализовал это сам. Вот код на случай, если кто-то заинтересован:
BOOL SomeClass::drawWavyUnderline (WPPoint ptTextPos, int nWidth, COLORREF 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!
// @param ptTextPos (in): TextOut reference point.
// @return: TRUE on success, FALSE on failure.
{
BOOL bResult = FALSE;
Gdiplus::Graphics *pGfx = NULL;
Gdiplus::Pen *pPen = NULL;
Gdiplus::PointF *arrPts = NULL;
// Determine the number of points required.
static const float fNumPixelsPerSample = 1.2f;
int nNumPts = (int)(nWidth / fNumPixelsPerSample);
if (nNumPts <= 1)
{
goto outpoint; // width too small or even negative!
}
// Retrieve information about the current GDI font.
TEXTMETRIC tm;
if (!::GetTextMetrics (/* HDC... */, &tm))
{
goto outpoint; // failed to retrieve TEXTMETRIC!
}
// Create points array.
arrPts = new Gdiplus::PointF [nNumPts];
if (arrPts == NULL)
{
goto outpoint; // out of mem!
}
// Fill points array.
static const float fYOffset = 1.0f;
static const float fAmp = 1.5f;
Gdiplus::PointF *pScan = arrPts;
for (int i = 0; i < nNumPts; i++, pScan++)
{
pScan->X = (Gdiplus::REAL)(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 * sin ((pScan->X / fNumPixelsPerSample)*(M_PI / 3.0)));
pScan->Y = (Gdiplus::REAL)(ptTextPos.y + tm.tmAscent + tm.tmDescent*0.5f + fYOffset + fValue);
}
// Create GDI+ graphics object.
HDC hdc = /* code to retrieve the HDC... */ ;
if (hdc == NULL)
{
goto outpoint; // missing HDC
}
pGfx = new Gdiplus::Graphics (hdc);
if (pGfx == NULL)
{
goto outpoint; // out of mem!
}
// Draw the lines.
pPen = new Gdiplus::Pen (Gdiplus::Color (GetRValue (col), GetGValue (col), GetBValue (col)));
if (pPen == NULL)
{
goto outpoint; // out of mem!
}
pGfx->SetSmoothingMode (Gdiplus::SmoothingModeHighQuality);
if (pGfx->DrawLines (pPen, arrPts, nNumPts) != Gdiplus::Ok)
{
goto outpoint; // failed to draw the lines!
}
bResult = TRUE;
outpoint:
// Clean up.
if (pPen != NULL) delete pPen;
if (pGfx != NULL) delete pGfx;
if (arrPts != NULL) delete[] arrPts;
return bResult;
}
PS: Если вам не нравится gotos, не стесняйтесь переписать его и использовать вместо этого try..catch!
Это избавило меня от лишних хлопот, спасибо, вот мое решение без gotos и небольшой оптимизации:
#include <gdiplus.h>
#include <corecrt_math_defines.h>
#include <vector>
bool drawWavyUnderline(HDC hDC, const POINT& ptTextPos, int nWidth, COLORREF 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|TA_TOP.
// @param ptTextPos (in): TextOut reference point.
// @return: TRUE on success, FALSE on failure.
{
bool bResult = false;
// Determine the number of points required.
const float fNumPixelsPerSample = 1.2f;
const int nPts = (int)(nWidth / fNumPixelsPerSample);
if (nPts > 1) {
try {
// Retrieve information about the current GDI font.
TEXTMETRIC tm;
if (::GetTextMetrics(hDC, &tm)) {
// Fill points array.
const float fAmp = 1.5f;
const float fYOffset = ptTextPos.y + tm.tmAscent + tm.tmDescent * 0.5f + 1.0f;
float xOffset = 0.0;
std::vector< Gdiplus::PointF> arrPts;
arrPts.resize(nPts);
for (auto& pt : arrPts) {
pt.X = (Gdiplus::REAL)(ptTextPos.x + xOffset++ * nWidth / (nPts - 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.
const float fValue = (float)(fAmp * sin((pt.X / fNumPixelsPerSample) * (M_PI / 3.0)));
pt.Y = (Gdiplus::REAL)(fYOffset + fValue);
}
// Draw the wavy line:
Gdiplus::Graphics gfx(hDC);
Gdiplus::Pen pen(Gdiplus::Color(GetRValue(col), GetGValue(col), GetBValue(col)));
gfx.SetSmoothingMode(Gdiplus::SmoothingModeHighQuality);
bResult = gfx.DrawLines(&pen, arrPts.data(), nPts) == Gdiplus::Ok;
}
}
catch (...) {
}
}
return bResult;
}
Вот моя версия на С#:
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;
}
}
}