【问题标题】:Scrolling NSTextView to bottom将 NSTextView 滚动到底部
【发布时间】:2013-03-21 11:55:01
【问题描述】:

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

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

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

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

我怎样才能以编程方式做到这一点?

【问题讨论】:

    标签: macos cocoa nstextview


    【解决方案1】:

    找到解决方案:

    - (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)];
    }
    

    【讨论】:

    • 好的,但我认为不需要“智能”BOOL 滚动条。首先,在 BOOL 滚动表达式中,运算符应该是 != 而不是 ==。有道理, != 对我有用,但 == 不起作用。其次,如果我添加一行文本,以换行符结尾,那么有时它会显示换行符,有时它不会。我不明白为什么我们会想要“滚动到文本视图内容的末尾”。这就是我们想要的。在所有情况下。我删除了 if (scroll) 行,它工作正常。也许我们正在测试相反的边缘情况:))
    • 小心使用“[self.textView scrollRangeToVisible: NSMakeRange(self.textView.string.length, 0)]”。这实际上可能不会滚动到底部(取决于 NSTextView 的布局。)如果 NSTextView 的高度不能被文本行的高度整除,它可能会部分切断文本的底线(其中情况下,智能滚动将不起作用。)更好地使用(Swift 示例):“self.textView.scrollToVisible( NSRect(x: 0, y: self.textView.frame.height-1, width: self.textView.frame .width, height: 1) )".
    • 另外,我发现对滚动标志的这种轻微修改很有帮助,它让你有点松懈,所以你不必处于确切的底部(Swift 示例):let scroll = abs(self.logTextView.visibleRect.maxY - self.logTextView.bounds.maxY) < kScrollThreshold(带有kScrollThreshold 设置为 25。)
    【解决方案2】:

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

    【讨论】:

    • 谢谢!你也可以传递 nil,而不是 self。
    • 我同意这在实践中有效,但我在 Apple 的文档中找不到任何参考,表明此 NSResponder 方法已由 NSTextView 正式实现(并支持)。可以给个链接吗?
    • 我不敢相信我已经搞砸了这两天......非常感谢!
    【解决方案3】:

    斯威夫特 4 + 5

    let smartScroll = self.textView.visibleRect.maxY == self.textView.bounds.maxY
    
    self.textView.textStorage?.append("new text")
    
    if smartScroll{
        self.textView.scrollToEndOfDocument(self)
    }
    
    

    【讨论】:

      【解决方案4】:

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

      我的解决方案允许您手动滚动,同时将输出添加到视图中。一旦滚动到 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);
      }
      

      【讨论】:

        【解决方案5】:

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

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2013-05-17
          • 1970-01-01
          • 2015-03-25
          • 2014-12-12
          • 1970-01-01
          • 2016-12-27
          • 2013-07-22
          • 2010-10-28
          相关资源
          最近更新 更多