表示しようとしている文字列がテキストフィールドに収まりきるか判断するには?


前回「Finderからファイルやフォルダをドラッグ&ドロップしてそのパスを表示するTextFieldを作ってみる」で作成したNSTextsFieldの拡張クラスですが、ディレクトリ構成の深い位置にあるファイルやフォルダをドロップした場合、テキストフィールドの幅をオーバーした部分がクリッピングされて見えなくなってしまうという不具合があります。
EditableをTRUEに設定しておけばカーソルを設定して→キーでスクロールさせるという手もありますが、かったるいですし、Editableでない場合はどうしようもありません。

この問題の解決策として、フィールド上にマウスカーソルがある場合、ToolTipでフルパスを表示させることにしました。

無条件に表示させるようにすることは簡単なのですが、パスがフィールド内に収まっているときにも表示されてしまうのはいまいちイケてない(死語?)ので、パスがフィールドの幅をオーバーしているときのみToolTipでフルパスを表示させるようにしたいと考えました。

1.基本的な情報の入手

まずどこから手を付けてよいのかわからないのでネットで検索してみると以下のサイトが見つかりました。

「CocoaDev- TruncatingStringsInTableView」
http://www.cocoadev.com/index.pl?TruncatingStringsInTableView

中身を全部解析していないのですが、TableViewにおいてカラム幅に収まらない文字列の場合途中で切り取って後ろに"..."を付加するような処理のサンプルだと思われるのできっとヒントがあるに違いないと思い、少しソースを追ってみました。
するとソースの3分の1くらいの位置にある以下のコードが目に付きました。

    // NSAttributedString instead, and use that as your cell value.
    NSDictionary * attributes = [[cell attributedStringValue] attributesAtIndex:0 effectiveRange:nil];
    double ellipsisWidth = [aString sizeWithAttributes:attributes].width;

これをみるとどうもNSAttributedStringというクラスが鍵を握っているようです。ドキュメントを見ると文字毎に属性(フォント、サイズ、スタイル、文字間隔 etc...)を持つ文字列を管理するクラスのようです。

上記ソース中の2行目を細かく分解して見てみます。

オブジェクトcellはNSCellクラスのインスタンスで、

  [cell attributedStringValue]

では、cellの文字とその属性を保持しているNSAttributedStringクラスのオブジェクトを取得しています。

// Cellの保持する文字列を属性付きで取得する
- (NSAttributedString *)attributedStringValue

次に取得したNSAttributedStringクラスのオブジェクトの以下のメソッドを呼び出しています。

// 指定した位置の文字属性を取得する
- (NSDictionary *)attributesAtIndex:(unsigned)index effectiveRange:(NSRangePointer)aRange

実際のソースではindexに0を指定していますのでスタイル付きのテキストではないことを前提に先頭の文字の属性を取得しています。effectiveRange:で指定するパラメータは同じ属性が適用される範囲情報を受け取るための領域アドレスなのですが、このケースでは不要なのでnilを指定しています。
これによりこのCellのフォントやサイズ等の文字の属性を含むNSDictionaryオブジェクトが取得されます。
取得される属性の具体的な内容はNSLogでダンプして確認してみてください。

次にNSStringの以下のメソッドで指定された属性で描画を行った場合の画面上の幅を取得します。

// 指定した描画属性での文字列幅の取得
- (NSSize)sizeWithAttributes:(NSDictionary *)attributes

なお、このメソッドはFoundationのNSStringではなく、「AppKit」の「NSString Additions」に記載されていますのでご注意ください。(*余談へ

ということはテキストフィールドが保持しているであろうNSAttributedStringクラスのオブジェクトが取得できれば描画に必要な幅が取得できるわけで、希望の光が見えてきました。

ではNSTextFieldにそのようなメソッドがあるか調べてみたところ、その親クラスであるNSControlがNSCellと同じメソッドを持っていることがわかりました。

// Controlの保持する文字列を属性付きで取得する
- (NSAttributedString *)attributedStringValue

これでお膳立てはそろいました。次は実装に取り掛かりましょう。

2.ToolTipでのフルパス表示機能を組み込む

組み込むと言ってもUCFilePathTextFieldクラスのドロップの実行時の処理(performDragOperation:)に以下のように判定とToolTipの設定処理を追加するだけです。

// ドロップ処理の実行要求
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
    // ドラッッグされたファイルの一覧取得
    NSArray *parrFiles = [self fileListInDraggingInfo:sender];
    if ( [parrFiles count] > 1 ) {
        // 複数のドラッグは受付不可
        // (draggingEntered:の時点ではじいてるのでまずないとは思うが念のため)
        return NO;
    }

    // ドラッグされたファイルパス取得
    NSString *pstrPath = [parrFiles objectAtIndex:0];

    // パス文字列をテキストフィールドに設定
    [self setStringValue:pstrPath];

    // 2004/07/26 T.Yamane Add Start
    // ToolTipによるフルパス表示機能追加
    // (1) このテキストフィールドの文字属性取得
    NSAttributedString *poAttrStr = [self attributedStringValue];
    NSDictionary *pdicAttr = [poAttrStr attributesAtIndex:0 effectiveRange:nil];
    NSLog( @"Char attr=%@\n", pdicAttr );

    // (2)文字列の描画幅とフィールドの幅を比較し、オーバーしていればToolTip設定
    NSSize size = [pstrPath sizeWithAttributes:pdicAttr];
    NSRect rect = [self bounds];
    if ( size.width > rect.size.width ) {
        // フィールド幅をオーバーする場合
        [self setToolTip:pstrPath];
    } else {
        // フィールド幅以下の場合
        [self setToolTip:nil];
    }
    // 2004/07/26 T.Yamane Add End
    
    return YES;
}

「// 2004/07/26 T.Yamane Add Start」〜「// 2004/07/26 T.Yamane Add End」に囲まれた部分が今回追加した部分です。

3.ビルド&実行

以上で作成作業は完了ですので、ビルド&実行してみてください。

 余談: 個人的な感想ですが、Cocoaの描画システムは描画ターゲット(あるいはコンテキスト)が明瞭ではなく、NSColorやNSStringなどFoundationの範疇のクラスに描画を担わせていたりと、とてもわかりづらいと感じます。
また、描画ターゲットが明瞭でないということはオブジェクトの排他制御を的確にコントロールできないわけですから、スレッドセーフにはなっていないのではないかと思っています。
正直言ってCocoaの描画システムは誰が何を管理しているのかいろんなクラスに分散している上に明確に見えないので好みではありません。

サンプルプログラムのダウンロード(24KB)

【作成・確認環境】
Mac OS X v10.3.4
Xcode v1.2


一覧に戻る