将NSTextView滚动到底部

时间:2013-03-21 11:55:02

标签: macos cocoa nstextview

我正在为OS X制作一个小型服务器应用程序,我正在使用NSTextView记录有关已连接客户端的一些信息。

每当我需要记录某些内容时,我就会以这种方式将新消息附加到NSTextView的文本中:

- (void)logMessage:(NSString *)message
{
    if (message) {
        self.textView.string = [self.textView.string stringByAppendingFormat:@"%@\n",message];
    }
}

在此之后我想要NSTextField(或者我应该说包含它的NSClipView)向下滚动以显示其文本的最后一行(显然它应该仅在最后一行不可见时滚动,在事实上,如果然后新行是我记录的第一行已经在屏幕上,所以不需要向下滚动)。

我该如何以编程方式执行此操作?

5 个答案:

答案 0 :(得分:15)

找到解决方案:

- (void)logMessage:(NSString *)message
{
    if (message) {
        [self appendMessage:message];
    }
}

- (void)appendMessage:(NSString *)message
{
    NSString *messageWithNewLine = [message stringByAppendingString:@"\n"];

    // Smart Scrolling
    BOOL scroll = (NSMaxY(self.textView.visibleRect) == NSMaxY(self.textView.bounds));

    // Append string to textview
    [self.textView.textStorage appendAttributedString:[[NSAttributedString alloc]initWithString:messageWithNewLine]];

    if (scroll) // Scroll to end of the textview contents
        [self.textView scrollRangeToVisible: NSMakeRange(self.textView.string.length, 0)];
}

答案 1 :(得分:8)

从OS 10.6开始,它就像nsTextView.scrollToEndOfDocument(self)一样简单。

答案 2 :(得分:2)

我已经搞砸了一段时间,因为我无法让它可靠地工作。我终于让我的代码工作了,所以我想把它作为回复发布。

我的解决方案允许您手动滚动,同时将输出添加到视图中。滚动到NSTextView的绝对底部后,自动滚动将立即恢复(如果已启用)。

首先只在需要时#import这个类别......

FSScrollToBottomExtensions.h:

@interface NSView (FSScrollToBottomExtensions)
- (float)distanceToBottom;
- (BOOL)isAtBottom;
- (void)scrollToBottom;
@end

FSScrollToBottomExtensions.m:

@implementation NSView (FSScrollToBottomExtensions)
- (float)distanceToBottom
{
    NSRect  visRect;
    NSRect  boundsRect;

    visRect = [self visibleRect];
    boundsRect = [self bounds];
    return(NSMaxY(visRect) - NSMaxY(boundsRect));
}

// Apple's suggestion did not work for me.
- (BOOL)isAtBottom
{
    return([self distanceToBottom] == 0.0);
}

// The scrollToBottom method provided by Apple seems unreliable, so I wrote this one
- (void)scrollToBottom
{
    NSPoint     pt;
    id          scrollView;
    id          clipView;

    pt.x = 0;
    pt.y = 100000000000.0;

    scrollView = [self enclosingScrollView];
    clipView = [scrollView contentView];

    pt = [clipView constrainScrollPoint:pt];
    [clipView scrollToPoint:pt];
    [scrollView reflectScrolledClipView:clipView];
}
@end

...创建一个" OutputView",它是NSTextView的子类:

FSOutputView.h:

@interface FSOutputView : NSTextView
{
    BOOL                scrollToBottomPending;
}

FSOutputView.m:

@implementation FSOutputView

- (id)setup
{
    ...
    return(self);
}

- (id)initWithCoder:(NSCoder *)aCoder
{
    return([[super initWithCoder:aCoder] setup]);
}

- (id)initWithFrame:(NSRect)aFrame textContainer:(NSTextContainer *)aTextContainer
{
    return([[super initWithFrame:aFrame textContainer:aTextContainer] setup]);
}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [super dealloc];
}

- (void)awakeFromNib
{
    NSNotificationCenter    *notificationCenter;
    NSView                  *view;

    // viewBoundsDidChange catches scrolling that happens when the caret
    // moves, and scrolling caused by pressing the scrollbar arrows.
    view = [self superview];
    [notificationCenter addObserver:self
    selector:@selector(viewBoundsDidChangeNotification:)
        name:NSViewBoundsDidChangeNotification object:view];
    [view setPostsBoundsChangedNotifications:YES];

    // viewFrameDidChange catches scrolling that happens because text
    // is inserted or deleted.
    // it also catches situations, where window resizing causes changes.
    [notificationCenter addObserver:self
        selector:@selector(viewFrameDidChangeNotification:)
        name:NSViewFrameDidChangeNotification object:self];
    [self setPostsFrameChangedNotifications:YES];

}

- (void)handleScrollToBottom
{
    if(scrollToBottomPending)
    {
        scrollToBottomPending = NO;
        [self scrollToBottom];
    }
}

- (void)viewBoundsDidChangeNotification:(NSNotification *)aNotification
{
    [self handleScrollToBottom];
}

- (void)viewFrameDidChangeNotification:(NSNotification *)aNotification
{
    [self handleScrollToBottom];
}

- (void)outputAttributedString:(NSAttributedString *)aAttributedString
    flags:(int)aFlags
{
    NSRange                     range;
    BOOL                        wasAtBottom;

    if(aAttributedString)
    {
        wasAtBottom = [self isAtBottom];

        range = [self selectedRange];
        if(aFlags & FSAppendString)
        {
            range = NSMakeRange([[self textStorage] length], 0);
        }
        if([self shouldChangeTextInRange:range
            replacementString:[aAttributedString string]])
        {
            [[self textStorage] beginEditing];
            [[self textStorage] replaceCharactersInRange:range
                withAttributedString:aAttributedString];
            [[self textStorage] endEditing];
        }

        range.location += [aAttributedString length];
        range.length = 0;
        if(!(aFlags & FSAppendString))
        {
            [self setSelectedRange:range];
        }

        if(wasAtBottom || (aFlags & FSForceScroll))
        {
            scrollToBottomPending = YES;
        }
    }
}
@end

...你可以为这个类增加一些方便的方法(我把它剥离了),这样你就可以输出一个格式化的字符串。

- (void)outputString:(NSString *)aFormatString arguments:(va_list)aArguments attributeKey:(NSString *)aKey flags:(int)aFlags
{
    NSMutableAttributedString   *str;

    str = [... generate attributed string from parameters ...];
    [self outputAttributedString:str flags:aFlags];
}

- (void)outputLineWithFormat:(NSString *)aFormatString, ...
{
    va_list         args;
    va_start(args, aFormatString);
    [self outputString:aFormatString arguments:args attributeKey:NULL flags:FSAddNewLine];
    va_end(args);
}

答案 3 :(得分:1)

快捷键4

let smartScroll = self.textView.visibleRect.maxY == self.textView.bounds.maxY

self.textView.textStorage?.append("new text")

if smartScroll{
    self.textView.scrollToEndOfDocument(self)
}

答案 4 :(得分:0)

我有一些自定义的NSTextView和自定义输入法,所以我的选择是使用:

self.scrollView.contentView.scroll(NSPoint(x: 1, y: self.textView.frame.size.height))