You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用NSRulerView为NSTextView绘制行号?

问题:Objective-C实现NSTextView行号显示时NSRulerView子类drawHashMarksAndLabels方法未调用

我需要基于NSRulerView用Objective-C实现NSTextView的行号显示功能,尝试了NoodleKit、STTextView等多个开源项目均无法正常运行。将NSTextView-LineNumberView中可运行的Swift示例代码转换为Objective-C后,发现NSRulerView子类中的drawHashMarksAndLabels:方法始终未被调用,导致行号无法显示。


1. NSRulerView子类定义

@interface PERulerView: NSRulerView
@end

@implementation PERulerView

- (id)initWithTextView:(NSTextView *)textView {
    NSScrollView *aScrollView = [textView enclosingScrollView];
    self = [super initWithScrollView:aScrollView orientation: NSVerticalRuler];
    
    if (self != nil) {
        self.clientView = textView;
        self.ruleThickness = 40;
    }

    return self;
}

- (void)drawHashMarksAndLabels:(NSRect)rect {
    // 从未被调用
    NSLog(@"drawHashMarksAndLabels");
    
    id obj = [self clientView];
    if ([obj isKindOfClass:[NSTextView class]]){
        [self doDrawHashMarksAndLabels: rect];
    }
}

- (void)doDrawHashMarksAndLabels:(NSRect)rect {
    static BOOL isDrawing = NO;
    if(isDrawing) return;
    
    NSTextView *textView = (NSTextView *)[self clientView];
    NSLayoutManager *layoutManager = [textView layoutManager];
    if(!layoutManager) return;

    isDrawing = YES;
    NSLog(@"doDrawHashMarksAndLabels");
    CGPoint relativePoint = [self convertPoint:NSZeroPoint fromView:textView];
    NSMutableDictionary *attr = [NSMutableDictionary new];
    attr[NSFontAttributeName] = textView.font;
    attr[NSForegroundColorAttributeName] = [NSColor redColor];

    static void(^drawLineNumber)(NSDictionary *a, NSString *lineNumberString, CGFloat y) = nil;
    if (!drawLineNumber) {
        drawLineNumber = ^(NSDictionary *a, NSString *lineNumberString, CGFloat y) {
            NSAttributedString *attString = [[NSAttributedString alloc] initWithString:lineNumberString attributes:a];
            NSSize stringSize = [lineNumberString sizeWithAttributes:a];
            // 计算行号在侧边栏的X位置
            CGFloat x = 40 - stringSize.width;
            // 绘制富文本
            [attString drawAtPoint:NSMakePoint(x, y)];
            NSLog(@"drawLineNumber %@.", lineNumberString);
        };
    }

    NSRange visibleGlyphRange = [layoutManager glyphRangeForBoundingRect:textView.visibleRect inTextContainer:textView.textContainer];
    NSUInteger firstVisibleGlyphCharacterIndex = [layoutManager characterIndexForGlyphAtIndex:visibleGlyphRange.location];
    NSRegularExpression *newLineRegex = [[NSRegularExpression alloc] initWithPattern:@"\\n" options:NSRegularExpressionCaseInsensitive error:nil];
    NSRange matchRange = NSMakeRange(0, firstVisibleGlyphCharacterIndex);
    NSArray *matches = [newLineRegex matchesInString:textView.string options:NSMatchingReportProgress range:matchRange];
    NSUInteger lineNumber = [matches count] + 1;
    NSLog(@"lineNumber: %ld", lineNumber);

    NSUInteger glyphIndexForStringLine = visibleGlyphRange.location;

    // 遍历可见区域内的每一行文本
    while(glyphIndexForStringLine < NSMaxRange(visibleGlyphRange)) {
        // 获取当前文本行的字符范围
        NSUInteger characterIndex = [layoutManager characterIndexForGlyphAtIndex:glyphIndexForStringLine];
        NSRange characterRangeForStringLine = [textView.string lineRangeForRange:NSMakeRange(characterIndex, 0)];
        
        NSRange glyphRangeForStringLine = [layoutManager glyphRangeForCharacterRange:characterRangeForStringLine actualCharacterRange:nil];
        
        NSUInteger glyphIndexForGlyphLine = glyphIndexForStringLine;
        NSUInteger glyphLineCount = 0;
        
        // 处理文本换行后的多行 glyph
        while(glyphIndexForGlyphLine < NSMaxRange(glyphRangeForStringLine)) {
            NSRange effectiveRange = NSMakeRange(0, 0);
            // 获取当前glyph行的绘制区域
            NSRect lineRect = [layoutManager lineFragmentRectForGlyphAtIndex:glyphIndexForGlyphLine effectiveRange:&effectiveRange withoutAdditionalLayout:YES];
            
            if(glyphLineCount > 0) {
                drawLineNumber(attr, @"-", NSMinY(lineRect)+relativePoint.y);
            } else {
                drawLineNumber(attr, [NSString stringWithFormat:@"%ld", lineNumber], NSMinY(lineRect)+relativePoint.y);
            }
            
            glyphLineCount += 1;
            glyphIndexForGlyphLine = NSMaxRange(effectiveRange);
        }
        
        glyphIndexForStringLine = NSMaxRange(glyphRangeForStringLine);
        lineNumber += 1;
    }

    // 绘制文本末尾的额外行
    if([layoutManager extraLineFragmentTextContainer] != nil) {
        drawLineNumber(attr, [NSString stringWithFormat:@"%ld", lineNumber], NSMinY(layoutManager.extraLineFragmentRect)+relativePoint.y);
    }
    
    isDrawing = NO;
}

@end

2. 视图控制器中初始化设置

@implementation ViewController
@property (nonatomic, strong) IBOutlet PETextView *textView;

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.textView initRulerView];
}

@end

3. NSTextView子类中初始化RulerView

@interface PETextView: NSTextView {
    PERulerView *rulerView;
}
- (void)initRulerView;
@end

@implementation PETextView

- (void)initRulerView {
    rulerView = [[PERulerView alloc] initWithTextView:self];
    NSScrollView *scrollView = [self enclosingScrollView];
    [scrollView setVerticalRulerView:rulerView];
    [scrollView setHasHorizontalRuler:NO];
    [scrollView setHasVerticalRuler:YES];
    [scrollView setRulersVisible:YES];

    self.postsFrameChangedNotifications = YES;
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(textViewFrameDidChange:)
                                                 name:NSViewFrameDidChangeNotification
                                               object:self];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(textViewTextDidChange:)
                                                 name:NSTextDidChangeNotification
                                               object:self];
}

- (void)textViewFrameDidChange:(NSNotification *)notif {
    [rulerView setNeedsDisplay:YES];
}

- (void)textViewTextDidChange:(NSNotification *)notif {
    [rulerView setNeedsDisplay:YES];
}

@end

运行环境

  • Xcode 16.2
  • macOS Sonoma 14.6.1

运行结果

运行后NSTextView左侧显示空白的行号侧边栏,无任何行号或标记绘制,控制台也未输出drawHashMarksAndLabels相关日志,证明该方法从未被调用。


内容的提问来源于stack exchange,提问作者Ashin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 12:47:02