Lee H
Lee H

Reputation: 11

How to convert cursor-position to text-position in Visual C?

Suppose I write a line of text in a variable-width font to a window with TextOut, and allow the user to click on any letter. Then how do I find out which part of the text he has clicked on? In other words, how do I convert the cursor-coordinates of his click to a string-offset?

I guess it could be done by calling GetTextExtentPoint32 on various string-truncations until I hit the right one, but surely there is a more efficient way. Microsoft's Notepad program knows exactly how many pixels to move when I right-arrow across a line - but how?

Upvotes: -1

Views: 551

Answers (1)

Lee H
Lee H

Reputation: 11

This answer was compiled by trial and error after wading through Microsoft's cryptic documentation.

The MSDN C library provides the following functions to display text:

* TextOut, which does not kern
* ExtTextOut, which can kern if its final parameter is non-null
* DrawText, which always kerns

If kerning is required (and on reflection I think that it is desirable) then it is a choice between ExtTextOut and DrawText.

DrawText provides a solution along the lines suggested by Groo. It requires a box to be drawn around the text-area, as in:

void textOut(HDC hdc, int x, int y, char *s, int l)
{
    RECT r = {0};
    r.left = x;
    r.right = Ewidth;
    r.top = y;
    r.bottom = y+LineHt;
    DrawText(hdc,s,l,&r,DT_NOPREFIX);
}

When the fontsize is set or changed, then a character-width lookup-table "CharW" must be built:

ABC CharW[256];     // char-width including leading/trailing space
    GetCharABCWidths(hdc, 0, 0xff, CharW);

When the font is changed, the kerning-table must be built:

KERNINGPAIR *Kern;  // pairs of chars and the (usually negative) additional gap between them
int KernCnt;        // number of same
KERNINGPAIR *CharK[256];// ptr to first kerning-pair for each char

    if(!Kern)
        free(Kern);
    KernCnt = GetKerningPairs(hdc, -1, 0);
    Kern = malloc(KernCnt * sizeof(*Kern));
    GetKerningPairs(hdc, KernCnt, Kern);
    {
        int i;
        for(i = 0; i < KernCnt; ++i) {
            KERNINGPAIR *k = Kern+i;
            if(k->wFirst < 0x100) {
                KERNINGPAIR **k2 = CharK + k->wFirst;
                if(!*k2)
                    *k2 = k;
            }
        }
    }

To play safe, the kerning table "Kern" should be sorted by (wFirst, wSecond), but it appears to be clustered by wFirst and therefore my code works without a qsort.

We can therefore calculate the pixel-width of any substring as follows:

int pixelWidth(char *s, int l)
{
    int x = 0;
    int i;
    for(i = 0; i < l; ++i) {
        char c = s[i];
        ABC *w = CharW+c;
        int wk = 0;
        if(i > 0) {
            char b = s[i-1];
            KERNINGPAIR *k = CharK[b];
            if(k)
            for(; k < Kern+KernCnt  &&  k->wFirst == b; ++k)
            if(k->wSecond == c)
                {wk = k->iKernAmount; break;}
        }
        x += wk + w->abcA + (w->abcB) + w->abcC;
    }
    return x;
}

This has been tested and agrees with the x-coordinate returned by DrawText when the maintain-current-coordinates flag is set:

    SetTextAlign(hdc,TA_UPDATECP)

It is therefore straightfoward to find the substring-length that matches a given pixel-width.

However, a simpler solution is provided by ExtTextOut:

INT W[512];     // maximum string-length

void textOut(HDC hdc, int x, int y, char *s, int l)
{
    GCP_RESULTS g={0};
    g.lStructSize = sizeof(g);
    g.lpDx = W;
    g.nGlyphs = sizeof(W)/sizeof(*W);
    GetCharacterPlacement(hdc, s, l, sizeof(W), &g, GCP_USEKERNING);
    ExtTextOut(hdc, x, y, 0, 0, s, l, g.lpDx);
}

The MSDN function GetCharacterPlacement() returns an array with the actual pixel-width for each character in the string s. It replaces my lookup-tables CharW, Kern, CharK above. According to Microsoft, it has been superseded by Uniscribe functions, though it still works fine for a European language like English.

Upvotes: 0

Related Questions