● Carbon視点でCocoa探求(2008/10/11)

  このニュースは、MOSAの会員にのみ配布されているデベロッパー向けの
  デジタルマガジンMOSADeNのに掲載された記事です。ほぼ一ヶ月遅れで
  ここに掲載されて行きます

 〜 様々な画像ファイル取り込み方法 〜


画像さえ登録してしまえば、ImageBrowserの扱いは簡単そうに見えます。しかし、その画像登録に色々なパタンが存在するので注意が必要です。今回は「しんぶんし3」でのドキュメントファイルや画像ファイルの取り扱いを復習しながら、登録ボタンで画像を追加していく処理を解説したいと思います。

ここで、もう一度「しんぶんし 3」のドキュメントファイルや画像ファイルの取り扱い処理の一覧を示してみます。

・ドキュメントファイルの場合

(1)ファイルメニューの「開く...」でファイルを読み込み内容をウィンドウに表示
(2)ファイルメニューの「保存」で内容をファイルへ書き出す
(3)ファイルメニューの「別名で保存...」で名称を変えてファイルへ書き出す
(4)ファイルメニューの「最後に保存した状態に戻す」で編集をキャンセルする
(5)未保存の編集済みウィンドウを閉じた時には「保存」処理を実行する
(6)終了時に未保存の編集済みウィンドウに対して「保存」処理を実行する
(7)アプリ・アイコンへのドロップで起動と内容を表示したウィンドウを開く
(8)新規や登録済みウィンドウにドロップすることでその内容を追加登録する
(9)ドキュメントのエイリアスファイルでも上記同等の処理を行えるようにする

・画像ファイルの場合

(1)画像追加ボタンでウィンドウに画像を追加登録
(2)ドラッグ&ドロップでウィンドウに画像を追加登録
(3)アプリ・アイコンへのドロップで新規ウィンドウを開き画像を登録
(4)画像ファイルが含まれたフォルダについても上記と同等の処理を行う
(5)画像ファイルのエイリアスファイルについても上記と同等の処理を行う

ドキュメントファイルの(1)から(7)までの処理は、Info.plistに対象ドキュメントのタイプや拡張子を正しく記載しておけば、NSDocumentクラスが、そのほとんどを自動処理します。(8)や(9)の一部、画像ファイルの(1)から(5)までの処理はすべて自分で実装します。また「ドキュメントファイルへの保存」に関する(2)(3)(5)(6)については、とりあえず画像取り込みとは直接関係ないので、まずは、それ以外の処理(ImageBrowserへの画像の登録に関わる処理)を順次実装して行きます。 順番としては(1)の「画像追加ボタンでウィンドウに画像を追加登録」から実装することにしましょう。


ユーザが画像追加ボタンをクリックすると、Interface Builderでリンクしておいたアクションメソッドが呼び出されます。今回の場合には、addImage:メソッドがそれに相当します。以下が、そのaddImage:メソッドです。最初に、NSOpenPanelクラスを用いてファイル選択用ダイアログを表示します。画像ファイルを選択できるようにするだけでなく、フォルダ(ディレクトリ)も選択可能に設定しておきます。ユーザが、ファイルやフォルダを選択しOKボタンをクリックしたら、そのパス名(NSString)を返してきます。

- (IBAction)addImage:(id)sender
{
  NSArray    *paths;        // パス名が保存される
  NSOpenPanel  *panel;

  panel=[NSOpenPanel openPanel];   // NSOPnePanleオブジェクトを得る
  [panel setCanChooseDirectories:YES]; // フォルダを選択可能に
  [panel setCanChooseFiles:YES];    // ファイルを選択可能に
  [panel setAllowsMultipleSelection:YES]; // 複数ファイルが選択可能に
  if( [panel runModalForTypes: [NSImage imageUnfilteredTypes]]==NSOKButton) // OKボタンをクリック
  {
    if( paths=[panel filenames] )   // 選択ファイルやフォルダのパスを得る
      [self addImagesPaths:paths]; // 実際の画像登録作業へ
  }
}

対象となるのは画像ファイルだけなので、それのみをダイアログに表示(選択可能に)するようにする必要があります。そのため、NSImageクラスのimageUnfilteredTypes:メソッドを使い、NSImageクラスで取り扱う事が可能な画像ファイルのUTI(Uniform Type Identifiers)情報の配列を得てNSOpenPanelに渡します。Carbonの場合には、スタンダードファイルダイアログを表示する時に、対象ファイルのファイルタイプの配列を渡していましたが、それとまったく同じ仕組みです。UTIについては、Apple社のデベロッパーサイトからダウンロードできる「Uniform Type Identifiers Overview」というドキュメントが参考になりますので、そちらを参照してみてください。

実際の画像登録作業ですが、大量の画像ファイルを登録する場合には、処理がかなり長時間となることが予想されます。ImageKit用に提供されているサンプルソースコードを見てみると、画像登録中にユーザが何も操作できなくなるのを避けるために、そうした処理を別スレッドで実行していました。「しんぶんし3」もそれを見習って、addImagesPaths: メソッドで実行する画像登録処理を別スレッドとします。Cocoaでのマルチスレッド処理は、 NSThreadクラスを使うことで簡単に実現できます。

- (void)addImagesPaths:(NSArray*)paths
{
  if( [paths count] ) // 配列個数を調べる
    [NSThread detachNewThreadSelector:@selector(addImagesThread:) toTarget:self withObject:paths];
}

まず、得られたパス(NSString)の配列個数を調べ、それがゼロならば何も実行しません。もしパスが得られていたら、 detachNewThreadSelector:toTarget:withObject:メソッドを使い、別スレッドとして起動するメソッドを指定します。この処理で作成されたスレッドは最初からデタッチされており、処理は終了してもメイン(親)スレッドとは合流しません。今回の場合、メソッドの実装先オブジェクトは自分自身(self)そしてメソッド名はaddImagesThread:とします。また、ユーザ情報オブジェクトとしてパス名の配列(paths)を対象スレッドへ渡しています。以下が、別スレッドとして起動するaddImagesThread:メソッドです。

- (void)addImagesThread:(NSArray*)paths
{
  NSAutoreleasePool *pool;
  NSInteger      i,ct;

  pool=[[NSAutoreleasePool alloc] init]; // NSAutoreleasePoolを用意
  ct=[paths count];           // パスの個数を得る
  for( i=0;i<ct;i++ )
    [self addImagesPath:[paths objectAtIndex:i]]; // 実際の画像登録処理

  [self performSelectorOnMainThread:@selector(updateDatasource) withObject:nil waitUntilDone:YES]; // 後処理
  [pool release]; // autoreleaseすべきオブジェクトをすべて解放
}

マルチスレッド処理で注意することは、スレッド内でスレッドセーフでないメソッドやルーチンを使用しない(もしくはNSLockクラスなどを使い制御して使用する)ことです。また、メインイベントから切り離されていますので、autorelease処理も単独で用意しなければいけません。ですから、入り口で NSAutoreleasePoolオブジェクトを作成し、スレッドを抜ける時にそれをreleaseします。メインスレッドから渡されたユーザ情報オブジェクト(今回はNSArray)は、スレッドに入る時に自動時にretainされ、抜ける時にrelaesaされますので、こちらも取り扱いに注意してください。

ユーザインターフェース制御などには多数の「スレッドセーフでない」クラスが関わりますので、別スレッドからそうしたクラスを利用する時には注意が必要です。加えて、メインスレッドで使っているメソッドをそのまま呼びたい場合も多々あります。そんな時には、performSelectorOnMainThread:withObject:waitUntilDone:メソッドを利用すると便利です。このメソッドを使い、メインスレッドのメソッド(スレッドセーフでなくてもOK)を呼び出します。今回、この仕組みにより呼び出されているのは 後処理を担当するupdateDatasourceメソッドです。

- (void)updateDatasource
{
  if( [temp count] ) // テンポラリ配列の要素があるか?
  {
    [list addObjectsFromArray:temp]; // テンポラリ配列をlistに追加する
    [temp removeAllObjects];     // テンポラリ配列の要素を削除する
    [imageBrowser reloadData];    // ImageBrowserを再描画
    [self updateChangeCount:NSChangeDone]; // ドキュメントを編集済みに
  }
}

次回は、 addImagesThread:のループ内から呼ばれている addImagesPathメソッドのソースコードを提示し解説することから画像登録処理の話を続けたいと思います。


copyright 2008 Ottimo, Inc. All rights reserved
無断転載・引用禁止