MFC Support for DirectWrite – Hit-Test

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 (1129 downloads)

MFC DirectWrite - Hit-Test Demo
MFC DirectWrite – Hit-Test Demo

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 (1252 downloads)

Resources and related articles

Leave a Comment