NSScrollViewの高さを入力文字数に応じて変更する


概要

以下のような、入力した文字数に応じてNSScrollViewとウィンドウの高さが変わるようなものを作成する

GitHub

NSTextViewのカスタムクラスを作成・設定する

  • 下記の通りクラスを割り当てておく

テキストが変更されたときの処理

// テキストの変更時に呼ばれる
- (void)didChangeText
{
    [self updateScrollViewHeight];  // TextViewの含まれるNSSCrollViewのframeのheightを更新する
    [self.delegate ChangeableHeightTextViewDidChange];   // 呼び出し元のWindowControllerでウィンドウの高さを変更する
}
  • NSTextViewのメソッドdidChangeTextをoverrideする
  • やることは下記の2点
  • NSTextViewの含まれるNSSCrollViewの高さを更新する(このクラスで処理する)
    • NSTextViewは自動で大きさが変わるようになっているみたいで、こちらは更新しない)
  • NSWindowControllerのdelegateメソッドを呼び出して、Windowのサイズを更新する(プロトコルを定義してdelegate先で処理する)

NSSCrollViewの高さを更新する

static const int kInitialStringHeight     = 19; // 1行目の文字列の高さ
static const int kSingleByteStringHeight  = 14; // 非日本語文字列の高さ(FontSiZeが12のとき)
//static const int kMultiByteStringHeight   = 18; // 日本語文字列の高さ (FontSiZeが12のとき)
static const int kMaximumLineNum          = 100; // この行数以上では高さを変更しない
  • 以降の計算で使用するマジックナンバーを変数で定義しておく。
    • (原始的…他にいい方法はないものか)
/**
 @brief NSTextViewの含まれるNSSCrollViewのframeのheightを更新する
 */
- (void)updateScrollViewHeight {
    // Calculate height
    NSUInteger numberOfLines = [self numberOfLines];
    NSUInteger height = 0;
    if (numberOfLines <= kMaximumLineNum) {
        height = kInitialStringHeight + (numberOfLines - 1) * kSingleByteStringHeight;  // 1行目の高さは固定としている
        if (height < kInitialStringHeight) {
            height = kInitialStringHeight;  // 最低の幅を設定
        }
    }

    // Update height
    NSScrollView *scrollView = (NSScrollView *) self.superview.superview;
    NSRect frame      = scrollView.frame;
    frame.size.height = height;
    scrollView.frame  = frame; // 実際のNSScrollViewのframeを更新
}

// Calculate height
NSUInteger numberOfLines = [self numberOfLines];
NSUInteger height = 0;
if (numberOfLines <= kMaximumLineNum) {
    height = kInitialStringHeight + (numberOfLines - 1) * kSingleByteStringHeight;  // 1行目の高さは固定としている
    if (height < kInitialStringHeight) {
        height = kInitialStringHeight;  // 最低の幅を設定
    }
}
  • 行数を計算する関数を作成しておく。(後述)
  • 行数がわかれば、あとは原始的に計算していく。
  • 今回は英語の文字列のみを考慮しているが、日本語を含む場合は1行あたりの高さが変わってくるのでバランスを取る必要があるか。
// Update height
NSScrollView *scrollView = (NSScrollView *) self.superview.superview;
NSRect frame      = scrollView.frame;
frame.size.height = height;
scrollView.frame  = frame; // 実際のNSScrollViewのframeを更新
  • NSTextViewの含まれるNSSCrollViewのframeをここで直接更新する
  • 計算したheightを用いて、NSScrollViewframeの高さを更新する
self.superview.superview.frame.size.height = height;

「myLabel.frame」はプロパティへのアクセスで、「frame.size」は構造体メンバへのアクセス。これを混ぜているからエラーとなっている。

NSTextViewの文字列の行数を計算する

/**
 @brief NSTextViewの文字列の行数を計算する
 */
- (NSInteger)numberOfLines {
    NSLayoutManager *layoutManager = [self layoutManager];
    NSUInteger      numberOfLines  = 0;     // 行数のカウント用変数
    NSUInteger      numberOfGlyphs = [layoutManager numberOfGlyphs];
    NSRange         lineRange;  // 現在対象となっている行の、先頭の開始位置と文字数
    for (NSUInteger index = 0; index < numberOfGlyphs; numberOfLines++) {   // index: 現在見ている文字の位置
        (void) [layoutManager lineFragmentRectForGlyphAtIndex:index
                                               effectiveRange:&lineRange];
        index = NSMaxRange(lineRange);
    }

    // 最終行が改行の場合に計算されていないようなので、その補正をする
    NSString *text = self.textStorage.string;
    if (text.length != 0 && [[text substringFromIndex:text.length - 1] isEqualToString:@"\n"]) {
        numberOfLines++;
    }
    return numberOfLines;
}

Protocolの定義

  • Windowの大きさを変えるために、NSWindowController用にプロトコルを定義する
@protocol ChangeableHeightTextViewDelegate <NSTextViewDelegate>
- (void)ChangeableHeightTextViewDidChange;  // テキストビューの内容が変更されたときによばれる
@end
  • <NSTextViewDelegate>NSTextViewDelegateを継承して新たなプロトコルを作成することを示す
@property (weak, nonatomic) id <ChangeableHeightTextViewDelegate> delegate;

※上記のようにエラーが出るのだが、既存のDelegateを継承する場合のdelegateプロパティの書き方が間違っているのだろうか?

NSTextViewの大きさを可視化する

- (void)drawRect:(NSRect)dirtyRect {
    [super drawRect:dirtyRect];

    // Drawing code here.
    // 見やすいようにNSTextViewの範囲を描画する
    NSRect bounds = [self bounds];
    [[NSColor greenColor] set];
    [NSBezierPath strokeRect:bounds];
}
  • NSTextViewの範囲がわかりやすいように描画を行っておく

ウィンドウの大きさを変更する

// MARK:- ChangeableHeightTextViewDelegate Delegate Methods
// TextView内のテキストが変更されたときに呼ばれる
- (void)ChangeableHeightTextViewDidChange {
    NSRect windowFrame = self.window.frame;
    NSRect scrollViewFrame = _myTextView.superview.superview.frame;
    // 20:Windowのヘッダ幅、上下のマージン、
    // 20:(入力していってね)の高さ
    // 6 :固定テキストとNSSCrollViewの間隔
    windowFrame.size.height = (20 + 20 + 6 + 20 + 20) + scrollViewFrame.size.height;
    [self.window setFrame:windowFrame display:YES];
}
  • ウィンドウの大きさは、ScrollViewの高さ+その他要素の高さで計算する