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