DirectWrite has hit-testing support that can be useful for showing a caret, making a selection, doing some action if the user chicks in a given text range, and so on. The hit-test methods of IDWriteTextLayout interface are HitTestPoint, HitTestTextPosition and HitTestTextRange. Let me show a simple example for each one.
Hit-testing a point
This example calls IDWriteTextLayout::HitTestPoint in the WM_LBUTTONDOWN message handler and keeps in mind the text position in which the user has clicked.
void CChildView::OnLButtonDown(UINT nFlags, CPoint point)
{
// ...
// Get IDWriteTextLayout interface from CD2DTextLayout object
IDWriteTextLayout* pTextLayout = m_pTextLayout->Get();
// pixel location X to hit-test,
// relative to the top-left location of the layout box.
FLOAT fPointX = static_cast<FLOAT>(point.x);
// pixel location Y to hit-test,
// relative to the top-left location of the layout box.
FLOAT fPointY = static_cast<FLOAT>(point.y);
// an output flag that indicates whether the hit-test location
// is at the leading or the trailing side of the character.
BOOL bIsTrailingHit = FALSE;
// an output flag that indicates whether the hit-test location
// is inside the text string
BOOL bIsInside = FALSE;
// output geometry fully enclosing the hit-test location
DWRITE_HIT_TEST_METRICS hitTestMetrics = { 0 };
HRESULT hr = pTextLayout->HitTestPoint(IN fPointX, IN fPointY,
OUT &bIsTrailingHit, OUT &bIsInside, OUT &hitTestMetrics);
if (SUCCEEDED(hr))
{
// keep in mind the hit-test text position
m_nCaretPosition = hitTestMetrics.textPosition;
Invalidate();
}
CWnd::OnLButtonDown(nFlags, point);
}
Hit-testing a text position
Further, use the previousy kept in mind text position and call IDWriteTextLayout::HitTestTextPosition when need to draw the caret.
void CChildView::_DrawCaret(CHwndRenderTarget* pRenderTarget)
{
ASSERT_VALID(m_pTextLayout);
ASSERT(m_pTextLayout->IsValid());
// Get IDWriteTextLayout interface from CD2DTextLayout object
IDWriteTextLayout* pTextLayout = m_pTextLayout->Get();
// flag that indicates whether the pixel location is of the
// leading or the trailing side of the specified text position
BOOL bIsTrailingHit = FALSE;
FLOAT fPointX = 0.0f, fPointY = 0.0f;
DWRITE_HIT_TEST_METRICS hitTestMetrics = { 0 };
HRESULT hr = pTextLayout->HitTestTextPosition(
IN m_nCaretPosition,
IN bIsTrailingHit,
OUT &fPointX,
OUT &fPointY,
OUT &hitTestMetrics);
if (SUCCEEDED(hr))
{
// Draw the caret
// Note: this is just for demo purpose and
// you may want to make something more elaborated here
CD2DRectF rcCaret(fPointX + TEXT_MARGIN, fPointY + TEXT_MARGIN,
fPointX + TEXT_MARGIN + 2, fPointY + TEXT_MARGIN + hitTestMetrics.height);
pRenderTarget->FillRectangle(&rcCaret,
&CD2DSolidColorBrush(pRenderTarget, CARET_COLOR));
}
}
Hit-testing a text range
And finally, here is an example of using IDWriteTextLayout::HitTestTextRange
BOOL CChildView::_TextRangeHitTest(const CPoint& point, const DWRITE_TEXT_RANGE& textRange)
{
ASSERT_VALID(m_pTextLayout);
ASSERT(m_pTextLayout->IsValid());
// Get IDWriteTextLayout interface from CD2DTextLayout object
IDWriteTextLayout* pTextLayout = m_pTextLayout->Get();
FLOAT nOriginX = 0.0f, nOriginY = 0.0f;
UINT32 nActualHitTestMetricsCount = 0;
// Call once IDWriteTextLayout::HitTestTextRange in order to find out
// the place required for DWRITE_HIT_TEST_METRICS structures
// See MSDN documentation:
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd371473(v=vs.85).aspx
HRESULT hr = pTextLayout->HitTestTextRange(textRange.startPosition, textRange.length,
nOriginX, nOriginY, NULL, 0, &nActualHitTestMetricsCount);
if (HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER) != hr)
return FALSE;
// Allocate enough room for all hit-test metrics.
std::vector<DWRITE_HIT_TEST_METRICS> vHitTestMetrics(nActualHitTestMetricsCount);
// Call IDWriteTextLayout::HitTestTextRange again to effectively
// get the DWRITE_HIT_TEST_METRICS structures
hr = pTextLayout->HitTestTextRange(textRange.startPosition, textRange.length,
nOriginX, nOriginY,
&vHitTestMetrics[0],
static_cast(vHitTestMetrics.size()),
&nActualHitTestMetricsCount);
if (FAILED(hr))
return FALSE;
for (UINT32 nIndex = 0; nIndex < nActualHitTestMetricsCount; nIndex++)
{
DWRITE_HIT_TEST_METRICS& hitTestMetrics = vHitTestMetrics[nIndex];
CRect rcHit((int)hitTestMetrics.left, (int)hitTestMetrics.top,
(int)hitTestMetrics.left + (int)hitTestMetrics.width,
(int)hitTestMetrics.top + (int)hitTestMetrics.height);
if (rcHit.PtInRect(point))
return TRUE;
}
return FALSE;
}It can be used, for example, to set the hand cursor when the mouse is moved over the given text range:
void CChildView::OnMouseMove(UINT nFlags, CPoint point)
{
DWRITE_TEXT_RANGE textRange = _GetHyperlinkTextRange();
if (_TextRangeHitTest(point, textRange))
::SetCursor(m_hCursorHand);
else
::SetCursor(m_hCursorArrow);
CWnd::OnMouseMove(nFlags, point);
}or can show some internet page when te user clicks on a “hyperlink”.
void CChildView::OnLButtonUp(UINT nFlags, CPoint point)
{
DWRITE_TEXT_RANGE textRange = _GetHyperlinkTextRange();
if (_TextRangeHitTest(point, textRange))
{
::ShellExecute(m_hWnd, _T("open"), TEXT_URL, NULL, NULL, SW_SHOWNORMAL);
}
CWnd::OnLButtonUp(nFlags, point);
}More details can be found in the demo examples attached to this article.
Demo projects
I’ve added the hit-test features to DirectWrite Static Control.
Download: Download: MFC Support for DirectWrite Demo (Part 9).zip (369)
Also here can be found a simpler application, only showing the DirectWrite hit-test features, to be easier understand: Simple DirectWrite Hit-Test Demo.zip (396)
Resources and related articles
- MSDN: IDWriteTextLayout::HitTestPoint method
- MSDN: IDWriteTextLayout::HitTestTextPosition method
- MSDN: IDWriteTextLayout::HitTestTextRange method
- MSDN: How to Perform Hit Testing on a Text Layout
- pauldotknopf/WindowsSDK7-Samples: PadWrite
