objective-c - NSRulerView 如何正确对齐行号与正文

标签 objective-c macos cocoa nstextview

我在 MacOS 中使用 NSRulerView,以便在 NSTextView 旁边显示行号。 两个 View 共享相同的字体和相同的字体大小,但是在 NSTextView 中,字符串渲染是自动管理的,在 NSRulerView 中,我需要计算正确的行号(这部分工作正常),然后在 drawHashMarksAndLabelsInRect 中渲染字符串。

我的问题是我无法在两个 View 之间正确对齐文本。对于某些字体,它工作正常,而对于其他字体,则存在明显的差异。

我实际使用的代码是:

#define BTF_RULER_WIDTH     40.0f
#define BTF_RULER_PADDING    5.0f

static inline void drawLineNumber(NSUInteger lineNumber, CGFloat y, NSDictionary *attributes, CGFloat ruleThickness) {
    NSString *string = [[NSNumber numberWithUnsignedInteger:lineNumber] stringValue];
    NSAttributedString *attString = [[NSAttributedString alloc] initWithString:string attributes:attributes];
    NSUInteger x = ruleThickness - BTF_RULER_PADDING - attString.size.width;

    [attString drawAtPoint:NSMakePoint(x, y)];
}

static inline NSUInteger countNewLines(NSString *s, NSUInteger location, NSUInteger length) {
    CFStringInlineBuffer inlineBuffer;
    CFStringInitInlineBuffer((__bridge CFStringRef)s, &inlineBuffer, CFRangeMake(location, length));

    NSUInteger counter = 0;
    for (CFIndex i=0; i < length; ++i) {
        UniChar c = CFStringGetCharacterFromInlineBuffer(&inlineBuffer, i);
        if (c == (UniChar)'\n') ++counter;
    }
    return counter;
}

@implementation BTFRulerView

- (instancetype)initWithBTFTextView:(BTFTextView *)textView {
    self = [super initWithScrollView:textView.enclosingScrollView orientation:NSVerticalRuler];
    if (self) {
        self.clientView = textView;

        // default settings
        self.ruleThickness = BTF_RULER_WIDTH;
        self.textColor = [NSColor grayColor];
    }
    return self;
}

- (void)drawHashMarksAndLabelsInRect:(NSRect)rect {
    // do not use drawBackgroundInRect for background color otherwise a 1px right border with a different color appears
    if (_backgroundColor) {
        [_backgroundColor set];
        [NSBezierPath fillRect:rect];
    }

    BTFTextView *textView = (BTFTextView *)self.clientView;
    if (!textView) return;

    NSLayoutManager *layoutManager = textView.layoutManager;
    if (!layoutManager) return;

    NSString *textString = textView.string;
    if ((!textString) || (textString.length == 0)) return;

    CGFloat insetHeight = textView.textContainerInset.height;
    CGPoint relativePoint = [self convertPoint:NSZeroPoint fromView:textView];
    NSDictionary *lineNumberAttributes = @{NSFontAttributeName: textView.font, NSForegroundColorAttributeName: _textColor};

    NSRange visibleGlyphRange = [layoutManager glyphRangeForBoundingRect:textView.visibleRect inTextContainer:textView.textContainer];
    NSUInteger firstVisibleGlyphCharacterIndex = [layoutManager characterIndexForGlyphAtIndex:visibleGlyphRange.location];

    // line number for the first visible line
    NSUInteger lineNumber = countNewLines(textString, 0, firstVisibleGlyphCharacterIndex)+1;
    NSUInteger glyphIndexForStringLine = visibleGlyphRange.location;

    // go through each line in the string
    while (glyphIndexForStringLine < NSMaxRange(visibleGlyphRange)) {
        // range of current line in the string
        NSRange characterRangeForStringLine = [textString lineRangeForRange:NSMakeRange([layoutManager characterIndexForGlyphAtIndex:glyphIndexForStringLine], 0)];
        NSRange glyphRangeForStringLine = [layoutManager glyphRangeForCharacterRange: characterRangeForStringLine actualCharacterRange:nil];

        NSUInteger glyphIndexForGlyphLine = glyphIndexForStringLine;
        NSUInteger glyphLineCount = 0;

        while (glyphIndexForGlyphLine < NSMaxRange(glyphRangeForStringLine)) {
            // check if the current line in the string spread across several lines of glyphs
            NSRange effectiveRange = NSMakeRange(0, 0);

            // range of current "line of glyphs". If a line is wrapped then it will have more than one "line of glyphs"
            NSRect lineRect = [layoutManager lineFragmentRectForGlyphAtIndex:glyphIndexForGlyphLine effectiveRange:&effectiveRange withoutAdditionalLayout:YES];

            // compute Y for line number
            CGFloat y = NSMinY(lineRect) + relativePoint.y + insetHeight;

            // draw line number only if string does not spread across several lines
            if (glyphLineCount == 0) {
                drawLineNumber(lineNumber, y, lineNumberAttributes, self.ruleThickness);
            }

            // move to next glyph line
            ++glyphLineCount;
            glyphIndexForGlyphLine = NSMaxRange(effectiveRange);
        }

        glyphIndexForStringLine = NSMaxRange(glyphRangeForStringLine);
        ++lineNumber;
    }

    // draw line number for the extra line at the end of the text
    if (layoutManager.extraLineFragmentTextContainer) {
        CGFloat y = NSMinY(layoutManager.extraLineFragmentRect) + relativePoint.y + insetHeight;
        drawLineNumber(lineNumber, y, lineNumberAttributes, self.ruleThickness);
    }
}

enter image description here

我认为问题在于随后传递给 drawLineNumber 函数的 y 计算。知道如何正确计算它吗?

最佳答案

我找到了一个解决方案,我认为它可能对其他人非常有用:

#define BTF_RULER_WIDTH     40.0f
#define BTF_RULER_PADDING    5.0f

static inline void drawLineNumberInRect(NSUInteger lineNumber, NSRect lineRect, NSDictionary *attributes, CGFloat ruleThickness) {
    NSString *string = [[NSNumber numberWithUnsignedInteger:lineNumber] stringValue];
    NSAttributedString *attString = [[NSAttributedString alloc] initWithString:string attributes:attributes];
    NSUInteger x = ruleThickness - BTF_RULER_PADDING - attString.size.width;

    // Offetting the drawing keeping into account the ascender (because we draw it without NSStringDrawingUsesLineFragmentOrigin)
    NSFont *font = attributes[NSFontAttributeName];
    lineRect.origin.x = x;
    lineRect.origin.y += font.ascender;

    [attString drawWithRect:lineRect options:0 context:nil];
}

static inline NSUInteger countNewLines(NSString *s, NSUInteger location, NSUInteger length) {
    CFStringInlineBuffer inlineBuffer;
    CFStringInitInlineBuffer((__bridge CFStringRef)s, &inlineBuffer, CFRangeMake(location, length));

    NSUInteger counter = 0;
    for (CFIndex i=0; i < length; ++i) {
        UniChar c = CFStringGetCharacterFromInlineBuffer(&inlineBuffer, i);
        if (c == (UniChar)'\n') ++counter;
    }
    return counter;
}

@implementation BTFRulerView

- (instancetype)initWithBTFTextView:(BTFTextView *)textView {
    self = [super initWithScrollView:textView.enclosingScrollView orientation:NSVerticalRuler];
    if (self) {
        self.clientView = textView;

        // default settings
        self.ruleThickness = BTF_RULER_WIDTH;
        self.textColor = [NSColor grayColor];
    }
    return self;
}

- (void)drawHashMarksAndLabelsInRect:(NSRect)rect {
    // do not use drawBackgroundInRect for background color otherwise a 1px right border with a different color appears
    if (_backgroundColor) {
        [_backgroundColor set];
        [NSBezierPath fillRect:rect];
    }

    BTFTextView *textView = (BTFTextView *)self.clientView;
    if (!textView) return;

    NSLayoutManager *layoutManager = textView.layoutManager;
    if (!layoutManager) return;

    NSString *textString = textView.string;
    if ((!textString) || (textString.length == 0)) return;

    CGFloat insetHeight = textView.textContainerInset.height;
    CGPoint relativePoint = [self convertPoint:NSZeroPoint fromView:textView];

    // Gettign text attributes from the textview
    NSMutableDictionary *lineNumberAttributes = [[textView.textStorage attributesAtIndex:0 effectiveRange:NULL] mutableCopy];
    lineNumberAttributes[NSForegroundColorAttributeName] = self.textColor;

    NSRange visibleGlyphRange = [layoutManager glyphRangeForBoundingRect:textView.visibleRect inTextContainer:textView.textContainer];
    NSUInteger firstVisibleGlyphCharacterIndex = [layoutManager characterIndexForGlyphAtIndex:visibleGlyphRange.location];

    // line number for the first visible line
    NSUInteger lineNumber = countNewLines(textString, 0, firstVisibleGlyphCharacterIndex)+1;
    NSUInteger glyphIndexForStringLine = visibleGlyphRange.location;

    // go through each line in the string
    while (glyphIndexForStringLine < NSMaxRange(visibleGlyphRange)) {
        // range of current line in the string
        NSRange characterRangeForStringLine = [textString lineRangeForRange:NSMakeRange([layoutManager characterIndexForGlyphAtIndex:glyphIndexForStringLine], 0)];
        NSRange glyphRangeForStringLine = [layoutManager glyphRangeForCharacterRange: characterRangeForStringLine actualCharacterRange:nil];

        NSUInteger glyphIndexForGlyphLine = glyphIndexForStringLine;
        NSUInteger glyphLineCount = 0;

        while (glyphIndexForGlyphLine < NSMaxRange(glyphRangeForStringLine)) {
            // check if the current line in the string spread across several lines of glyphs
            NSRange effectiveRange = NSMakeRange(0, 0);

            // range of current "line of glyphs". If a line is wrapped then it will have more than one "line of glyphs"
            NSRect lineRect = [layoutManager lineFragmentRectForGlyphAtIndex:glyphIndexForGlyphLine effectiveRange:&effectiveRange withoutAdditionalLayout:YES];

            // compute Y for line number
            CGFloat y = ceil(NSMinY(lineRect) + relativePoint.y + insetHeight);
            lineRect.origin.y = y;

            // draw line number only if string does not spread across several lines
            if (glyphLineCount == 0) {
                drawLineNumberInRect(lineNumber, lineRect, lineNumberAttributes, self.ruleThickness);
            }

            // move to next glyph line
            ++glyphLineCount;
            glyphIndexForGlyphLine = NSMaxRange(effectiveRange);
        }

        glyphIndexForStringLine = NSMaxRange(glyphRangeForStringLine);
        ++lineNumber;
    }

    // draw line number for the extra line at the end of the text
    if (layoutManager.extraLineFragmentTextContainer) {
        NSRect lineRect = layoutManager.extraLineFragmentRect;
        CGFloat y = ceil(NSMinY(lineRect) + relativePoint.y + insetHeight);
        lineRect.origin.y = y;
        drawLineNumberInRect(lineNumber, lineRect, lineNumberAttributes, self.ruleThickness);
    }
}

我使用 drawWithRect 而不是 drawAtPoint,并且我直接使用连接的 textView 中的属性。

enter image description here

关于objective-c - NSRulerView 如何正确对齐行号与正文,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40672218/

相关文章:

ios - 使用 NSUserDefaults 或 CoreData 存储事件日历

iphone - 确定屏幕上的点是否在特定的 UIScrollView subview 内

macos - 条件绑定(bind)中的电子邮件验证绑定(bind)值必须是可选类型

cocoa - 延迟获取和维护排序的 NSArrayController

cocoa - 获取 NSImageView 中 NSImage 的边界

ios - 如何在 UITableView 中移动没有 NSIndexPath 的单元格?

iphone - iOS 中的 UIImage 或 CGImage API 是否可以进行这种屏蔽?

swift - 是否可以隐藏 "show package contents"中的某些文件?

macos - NSSearchField 带有左对齐占位符

swift - 实现可散列和重载运算符的协议(protocol)