在QuartzCore中沿路径绘制文本

8

假设我有一个由点组成的线数组和一段文本。如何在这条线上绘制文本?

 - (void)drawRect:(CGRect)rect 

如何在UIView中绘制文本?

我已经成功绘制了路径。我是否忽略了某个标准方法或框架,可以让我在该路径上绘制文本?理想情况下,我希望只使用QuartzCore/CoreGraphics实现此目的。

我尝试计算每个字符的宽度并旋转每个字符。这种方法有点可行,但我想知道是否有更优雅的方法。


我会选择手动调整路径上每个字符的位置和旋转,就像你提到的那样。 - Dimitris
如果这条线是直线,只需应用旋转和平移的CTM。 - kennytm
@KennyTM 这条路径将由几条直线组成,文本将覆盖这条路径的一个或多个线段。 - Felix Lamouroux
3个回答

11

我相信在Mac OS X上你可以做到这一点,但在iPhone上最接近的方法是使用CGContextShowGlyphsWithAdvances,但它甚至不能旋转。

使用循环并使用类似以下内容绘制每个字符不应该太难。这是从苹果的文档中改编而来,未经测试,请注意:

CGContextSelectFont(myContext, "Helvetica", 12, kCGEncodingMacRoman);
CGContextSetCharacterSpacing(myContext, 10);
CGContextSetTextDrawingMode(myContext, kCGTextFillStroke);
CGContextSetRGBFillColor(myContext, 0, 0, 0, 1);
CGContextSetRGBStrokeColor(myContext, 0, 0, 0, 1);

NSUInteger charIndex = 0;
for(NSString *myChar in arrayOfChars) {
    char *cString = [myChar UTF8String];
    CGPoint charOrigin = originForPositionAlongPath(charIndex, myPath);
    CGFloat slope = slopeForPositionAlongPath(charIndex, myPath);

    CGContextSetTextMatrix(myContext, CGAffineTransformMakeRotation(slope));
    CGContextShowTextAtPoint(myContext, charOrigin.x, charOrigin.y, cString, 1);
}

编辑:这里是PositionAlongPath函数的一个想法。虽然它们没有经过测试,但应该很接近。如果路径用尽,则originAlong...返回(-1,-1)。

CGPoint originForPositionAlongPath(int index, NSArray *path) {
    CGFloat charWidth = 12.0;
    CGFloat widthToGo = charWidth * index;

    NSInteger i = 0;
    CGPoint position = [path objectAtIndex:i];

    while(widthToGo >= 0) {
            //out of path, return invalid point
        if(i >= [path count]) {
            return CGPointMake(-1, -1);
        }

        CGPoint next = [path objectAtIndex:i+1];

        CGFloat xDiff = next.x - position.x;
        CGFloat yDiff = next.y - position.y;
        CGFloat distToNext = sqrt(xDiff*xDiff + yDiff*yDiff);

        CGFloat fracToGo = widthToGo/distToNext
            //point is before next point in path, interpolate the answer
        if(fracToGo < 1) {
            return CGPointMake(position.x+(xDiff*fracToGo), position.y+(yDiff*fracToGo));
        }

            //advance to next point on the path
        widthToGo -= distToNext;
        position = next;
        ++i;
    }
}


CGFloat slopeForPositionAlongPath(int index, NSArray *path) {
    CGPoint begin = originForPositionAlongPath(index, path);
    CGPoint end = originForPositionAlongPath(index+1, path);

    CGFloat xDiff = end.x - begin.x;
    CGFloat yDiff = end.y - begin.y;

    return atan(yDiff/xDiff);
}

我想困难的部分是找到两个函数 originForPositionAlongPath 和 slopeForPositionAlongPath。 - Felix Lamouroux
我不确定你尝试的是否与我添加的内容相似。如果你能告诉我为什么你尝试的方法不够好,我会看看能否改进我想出来的方法。 - David Kanarek
我之前只为给定的两个路径段实现了一个测试用例,但成功地制定了你编写的这两种通用方法。 我今天稍后会去检查它们。 非常感谢您的帮助。 我会回来提供更多反馈意见。 - Felix Lamouroux
有进展了吗?我很想看看最终解决方案的效果如何。 - David Kanarek
这个有用吗?因为我有一个类似的问题需要帮助。 - Wim Haanstra
谢谢!这个很好用。只需要微调一下。斜率和原点函数有包含CGPoint的NSArray,但是由于CGPoint是一个结构体,所以这是不可能的。我将它们改为了标准的C数组,并添加了第三个参数,即CGPoint数组的长度。 - Aaron

1

谢谢,但那并不完全是我所追求的,因为我想能够在QuartzCore内完成这个功能。 - Felix Lamouroux

0

上述示例不幸地没有按预期工作。

现在我终于找到了沿路径绘制文本的正确方法。

让我们开始吧:

您不能将此代码1:1采用,因为它只是从我的应用程序中摘录出来的,但我会通过一些注释来澄清事情。

// MODIFIED ORIGIN FUNCTION

CGPoint originForPositionAlongPath(float *l, float nextW, int index, NSArray *path) {
CGFloat widthToGo = *l + nextW;


NSInteger i = 0;
CGPoint position = [[path objectAtIndex:i] CGPointValue];

while(widthToGo >= 0) {
    //out of path, return invalid point
    if(i+1 >= [path count]) {
        return CGPointMake(-1, -1);
    }

    CGPoint next = [[path objectAtIndex:i+1] CGPointValue];

    CGFloat xDiff = next.x - position.x;
    CGFloat yDiff = next.y - position.y;
    CGFloat distToNext = sqrt(xDiff*xDiff + yDiff*yDiff);

    CGFloat fracToGo = widthToGo/distToNext;
    //point is before next point in path, interpolate the answer
    if(fracToGo < 1) {
        return CGPointMake(position.x+(xDiff*fracToGo), position.y+(yDiff*fracToGo));
    }

    //advance to next point on the path
    widthToGo -= distToNext;
    position = next;
    ++i;
}
}

// MODIFIED SLOPE FUNCTION

CGFloat slopeForPositionAlongPath(float* l, float nextW, int index, NSArray *path) {
CGPoint begin = originForPositionAlongPath(l, 0, index, path);
CGPoint end = originForPositionAlongPath(l, nextW, index+1, path);

CGFloat xDiff = end.x - begin.x;
CGFloat yDiff = end.y - begin.y;

return atan(yDiff/xDiff);
}


// IMPORTANT: CHARACTER WIDTHS FOR HELVETICA, ABOVE EXAMPLE USES FIXED WIDTHS WHICH IS NOT ACCURATE

float arraychars[] = {
0,     0,     0,     0,     0,     0,     0,     0,    
0,     0,     0,     0,     0,     0,     0,     0,    
0,     0,     0,     0,     0,     0,     0,     0,    
0,     0,     0,     0,     0,     0,     0,     0,    
0.278, 0.333, 0.474, 0.556, 0.556, 0.889, 0.722, 0.278,
0.333, 0.333, 0.389, 0.584, 0.278, 0.584, 0.278, 0.278,
0.556, 0.556, 0.556, 0.556, 0.556, 0.556, 0.556, 0.556,
0.556, 0.556, 0.333, 0.333, 0.584, 0.584, 0.584, 0.611,
0.975, 0.722, 0.722, 0.722, 0.722, 0.667, 0.611, 0.778,
0.722, 0.278, 0.556, 0.722, 0.611, 0.833, 0.722, 0.778,
0.667, 0.778, 0.722, 0.667, 0.611, 0.722, 0.667, 0.944,
0.667, 0.667, 0.611, 0.333, 0.278, 0.333, 0.584, 0.556,
0.278, 0.556, 0.611, 0.556, 0.611, 0.556, 0.333, 0.611,
0.611, 0.278, 0.278, 0.556, 0.278, 0.889, 0.611, 0.611,
0.611, 0.611, 0.389, 0.556, 0.333, 0.611, 0.556, 0.778,
0.556, 0.556, 0.5,   0.389, 0.28,  0.389, 0.584, 0,    
0,     0,     0,     0,     0,     0,     0,     0,    
0,     0,     0,     0,     0,     0,     0,     0,    
0.278, 0.333, 0.333, 0.333, 0.333, 0.333, 0.333, 0.333,
0.333, 0,     0.333, 0.333, 0,     0.333, 0.333, 0.333,
0.278, 0.333, 0.556, 0.556, 0.556, 0.556, 0.28,  0.556,
0.333, 0.737, 0.37,  0.556, 0.584, 0.333, 0.737, 0.333,
0.4,   0.584, 0.333, 0.333, 0.333, 0.611, 0.556, 0.278,
0.333, 0.333, 0.365, 0.556, 0.834, 0.834, 0.834, 0.611,
0.722, 0.722, 0.722, 0.722, 0.722, 0.722, 1,     0.722,
0.667, 0.667, 0.667, 0.667, 0.278, 0.278, 0.278, 0.278,
0.722, 0.722, 0.778, 0.778, 0.778, 0.778, 0.778, 0.584,
0.778, 0.722, 0.722, 0.722, 0.722, 0.667, 0.667, 0.611,
0.556, 0.556, 0.556, 0.556, 0.556, 0.556, 0.889, 0.556,
0.556, 0.556, 0.556, 0.556, 0.278, 0.278, 0.278, 0.278,
0.611, 0.611, 0.611, 0.611, 0.611, 0.611, 0.611, 0.584,
0.611, 0.611, 0.611, 0.611, 0.611, 0.556, 0.611, 0.556,
                 };    

void o_DrawContourLabel(void *myObjectInstance, TransBuffer* transBuffer,     MapPainterIphone* mp,const Projection& projection,
                    const MapParameter& parameter,
                    const LabelStyle& style,
                    const std::string& text,
                    size_t transStart, size_t transEnd){
// HERE WE Initialize the font etc.

CGContextSelectFont(context, "Helvetica-Bold", 10, kCGEncodingMacRoman);
CGContextSetCharacterSpacing(context, 0);
CGContextSetTextDrawingMode(context, kCGTextFillStroke);
CGContextSetLineWidth(context, 3.0);
CGContextSetRGBFillColor(context, style.GetTextR(), style.GetTextG(), style.GetTextB(), style.GetTextA());
CGContextSetRGBStrokeColor(context, 1, 1, 1, 1);

// Here we prepare a NSArray holding all waypoints of our path.
// I fill it from a "transBuffer" but you may fill it with whatever you want

NSMutableArray* path = [[NSMutableArray alloc] init];
if (transBuffer->buffer[transStart].x<transBuffer->buffer[transEnd].x) {
    for (size_t j=transStart; j<=transEnd; j++) {
        if (j==transStart) {
            [path addObject:[NSValue valueWithCGPoint:CGPointMake(transBuffer->buffer[j].x,transBuffer->buffer[j].y)]];
        }
        else {
            [path addObject:[NSValue valueWithCGPoint:CGPointMake(transBuffer->buffer[j].x,transBuffer->buffer[j].y)]];
        }
    }
}
else {
    for (size_t j=0; j<=transEnd-transStart; j++) {
        size_t idx=transEnd-j;

        if (j==0) {
            [path addObject:[NSValue valueWithCGPoint:CGPointMake(transBuffer->buffer[idx].x,transBuffer->buffer[idx].y)]];

        }
        else {
            [path addObject:[NSValue valueWithCGPoint:CGPointMake(transBuffer->buffer[idx].x,transBuffer->buffer[idx].y)]];

        }
    }
}

// if path is too short for "estimated text length" then exit

if (pathLength(path)<text.length()*7) {
    // Text is longer than path to draw on
    return;
}

// NOW PAINT CHAR FOR CHAR USING CHARACTER WIDTHS TABLE

float lenUpToNow = 0;

CGAffineTransform transform=CGAffineTransformMake(1.0, 0.0, 0.0, -1.0, 0.0, 0.0);
NSUInteger charIndex = 0;
for(int i=0;i<text.length();i++) {
    char *cString = (char*)malloc(2*sizeof(char));
    cString[0] = text.at(i);
    cString[1]=0;

    float nww = arraychars[cString[0]]*8*1.4;

    CGPoint charOrigin = originForPositionAlongPath(&lenUpToNow, 0, charIndex, path);
    CGFloat slope = slopeForPositionAlongPath(&lenUpToNow, nww, charIndex, path);
    std::cout << " CHARACTER " << cString << " placed at " << charOrigin.x << "," << charOrigin.y << std::endl;

    // DO NOT FORGET TO DO THIS (TWO TRANSFORMATIONS) .. one for the rotation
    // and one for mirroring, otherwise the text will be mirrored due to a
    // crappy coordinate system in QuartzCore.

    CGAffineTransform ct = CGAffineTransformConcat(transform,CGAffineTransformMakeRotation(slope));
    CGContextSetTextMatrix(context, ct);
    CGContextSetTextDrawingMode(context, kCGTextStroke);
    CGContextShowTextAtPoint(context, charOrigin.x, charOrigin.y, cString, 1);

    CGContextSetTextDrawingMode(context, kCGTextFill);
    CGContextShowTextAtPoint(context, charOrigin.x, charOrigin.y, cString, 1);
    std::cout << " ... len (" << (int)cString[0] << ") = " << arraychars[cString[0]] << " up to now: " << lenUpToNow << std::endl;
    lenUpToNow += nww;

    charIndex++;
    free(cString);
}


}

1
我认为我们最终使用了 NSString 方法来确定不同字体字符的宽度(例如 sizeWithFont:)。我们的主要问题是 CGContextShowTextAtPoint 不支持非拉丁字符。 - Felix Lamouroux
我在使用非拉丁字符时遇到了同样的问题。我使用了以下方法来解决它: - Daniel Cagara
NSString* strCC = [NSString stringWithUTF8String:label.text.c_str()]; const char* plCC = [strCC cStringUsingEncoding:NSMacOSRomanStringEncoding]; 然后绘制 plCC ... 就像个魅力一样。 - Daniel Cagara

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接