HMDT - Logic and Intuition -

about HMDT

Cocoa Programming Tips 1001

Application Kit

NSAplication

Application Kit - NSApplication

実行中のインスタンスを取得する

Keywords: sharedApplication, NSApp

Mac OS X では、モーダルダイアログは 2 つに分けられるんだ。1 つはアプリケーションからの警告を表示したりする、アプリケーション・モーダルダイアログ。もう 1 つは、ドキュメントに対するメッセージを出す(セーブするかい?とか)、ドキュメント・シートダイアログだ。前者は、いままで通りのウィンドウ型のダイアログ。後者は、シートっていう、ウィンドウのタイトルから、巻いてあるのが降りてくるように出てくるやつのことだ。

この 2 つは、両方ともモーダルダイアログなんだけど、実装方法がかなり違う。比べてみよう。

Application Kit - NSApplication

アプリケーション・モーダルダイアログを表示する

keywords: runModalForWindow

NSApplication ってのは、その名の通り、アプリケーションの中心となるクラスだ。実行中のアプリケーションは NSApplication のインスタンスを 1 つ持つことになるんだ。では、そのインスタンにアクセスするにはどうすればいいのか?方法は 2 つある。1 つは、クラスメソッド sharedApplication を使う方法。

Application Kit/NSApplication.h

+ (NSApplication *)sharedApplication;

これを使うと、インスタンスが返ってくる。もう 1 つは、グローバル変数 NSApp を使う方法。この変数は、NSApplication.h で宣言されている。

Application Kit/NSApplication.h

APPKIT_EXTERN id NSApp;

ただ単に NSApplication のインスタンスへの参照が欲しいならば、NSApp を使うのが簡単だよ。


Application Kit - NSApplication

NSApplicationMain の内部

Keywords: NSApplicationMain, sharedApplication, NSApp

ProjectBuilder を使って、Cocoa アプリケーションを作ると、main.m っていうファイルができて、次のようなコードが書かれるよね。

main.m (created by Project Builder)

int main(int argc, const char *argv[]) {
    return NSApplicationMain(argc, argv)
}

つまり、NSApplicationMain() っていう関数を呼び出しているんだ。これは何?NSApplication のドキュメントによれば、この関数は次のような関数と同値らしい(これそのものではない)。

Pseudo main (sample)

void NSApplicationMain(int argc, char *argv[]) {
    [NSApplication sharedApplication];
    [NSBundle loadNibNamed:@"myMain" owner:app];
    [NSApp run];
}

まず、sharedApplication メソッドを呼ぶ。このメソッドは、初めて呼ばれたときは、NSApplication のインスタンスを作るんだ。そして、グローバル NSApp に代入して、その値を返す。2 回目以降呼ばれたときは、ただ単に前に作ったインスタンスを返す。次に nib をロードする。最後に run メソッドを呼ぶ。run メソッドではイベントループが回っていて、Window Server からイベントを受け取ったりするわけだ。

Application Kit - NSApplication

マウスドラッグをトラッキングする

Keywords: nextEventMatchingMask

マウスのクリックやドラッグは、NSResponder のメソッドである、mouseDown:mouseDragged: でつかまえることができる。でも、同じコンテキストの中でドラッグを扱えたら便利じゃない?つまり、mouseDown: が呼び出されたら、そのメソッドの内部でマウスドラッグをトラッキングしてやる、というケースだ。それには nextEventMatchingMask:untilData:inMode:dequeue: を使えばできるんだ。

Application Kit/NSApplication.h

- (NSEvent*)nextEventMatchingMask:(unsigned int)mask
        untilData:(NSData*)expiration
        inMode:(NSString*)mode
        dequeue:(BOOL)deqFlag;

このメソッドを呼ぶと、望みのイベントが来るまで、アプリケーションの実行をそこでブロックさせることができるんだ。だから、永遠に来ないイベントを待ってたりすると、そこで固まることになるので、注意。マウスドラッグをトラッキングするには、マウスドラッグイベントと、マウスアップイベントを補足すればいい。マウスドラッグだったら、このメソッドを呼び続けて、マウスアップだったら、そこでトラックを終える。次のようなコードになる。

(sample)

- (void)mouseDown:(NSEvent*)event
{
    while(1) {
        event = [NSApp nextEventMatchingMask:
                (NSLeftMouseDraggeMaskt | NSLeftMouseUpMask)
            untilDate:[NSDate distantFuture] 
            inMode:NSEventTrackingRunLoopMode
            dequeue:YES];

        NSLog("Current mouse point:%@¥n", 
            NSStringFromPoint([event locationInWindow]);

        // Tracking process

        if([event type] == NSLeftMouseUp) {
            break;
        }
    }
}

ここでは、最初のマウスプレスを捕らえるために、mouseDown: をオーバーライドしている。そして、その中でループを回している。下手な処理を書くと、この中から抜け出れなくなるので注意。ここで、nextEventMatchingMask:untileDate:inMode:dequeue: を使って、イベントを取得している。maskNSLeftMouseDraggedMaskNSLeftMouseUpMask を指定してるんだ。expiration には NSDate の distantFuture を指定することにより、永遠に待つ。ここで、マウスドラッグか、マウスアップが起こると、event が返ってくるんだ。そのイベントをもとに、適当な処理を行う。ここでは、とりあえず位置を表示させた。最後に、もしイベントがマウスアップだったら、このループを抜ける。

このメソッド使わなくても、mouseDragged: を使えば、マウスのトラッキングはできる。どちらを使った方がいいのかは、アプリケーション次第。

Application Kit - NSApplication

モーダルダイアログ

Keywords: application-modal dialog, document-modal dialog

Mac OS X では、モーダルダイアログは 2 つに分けられるんだ。1 つはアプリケーションからの警告を表示したりする、アプリケーション・モーダルダイアログ。もう 1 つは、ドキュメントに対するメッセージを出す(セーブすしますか?とか)、ドキュメント・シートダイアログだ。前者は、いままで通りのウィンドウ型のダイアログ。後者は、シートっていう、ウィンドウのタイトルから、巻いてあるのが降りてくるように出てくるやつのことだ。

この 2 つは、両方ともモーダルダイアログなんだけど、実装方法がかなり違う。比べてみよう。

Application Kit - NSApplication

アプリケーション・モーダルダイアログを表示する

Keywords: runModalForWindow

アプリケーション・モーダルダイアログは、Classic のモーダルダイアログと似た感じの、ウィンドウを表示するタイプだ。これを表示するには、runModalForWindow: を使う。

Application Kit/NSApplication.h

- (int)runModalForWindow:(NSWindow*)window;

このメソッドを使うときは、呼び出しは同期的だということに注意してくれ。。どういうことかっていうと、このメソッドを呼ぶとダイアログが表示される。そして、そのダイアログを処理している間は、イベントループはその中で回るんだ。メソッドの呼び出しは、処理中は返ってこないで、ブロックされるんだ。

ここから抜けるには、stopModalstopModalWithCode: を、そのダイアログ処理の中で呼ぶ。外から止めたい場合には abortModal を使う。

Application Kit/NSApplication.h

- (void)stopModal;
- (void)stopModalWithCode:(int)returnCode;
- (void)abortModal;

これらを呼ぶと、runModalForWindow: のモーダルループから抜け出す。その際の返り値で、どのメソッドが呼ばれたかを判断することができるんだ。stopModalNSRunStoppedResponse を返す。stopModalWithCode: は、引数として渡された値を返す。そして、abortModal は、NSRunAbortedResponder を返すんだ。

これを利用して、ダイアログで押されたボタンを知るサンプルを紹介しよう。

Dialog/AppDelegate.m (sample)

- (IBAction)showDialog:(id)sender
{
    int	result;
    
    // Display modal dialog
    result = [[NSApplication sharedApplication] 
            runModalForWindow:_modalDialog];
    [_modalDialog orderOut:self];
    
    if(result == DIALOG_CANCEL) {
        // Cancel button was pushed
        NSLog(@"Dialog is canceled");
        return;
    }
    else if(result == DIALOG_OK) {
        // OK button was pushed
        NSLog(@"Dialog is accepted");
    }
    else if(result == NSRunAbortedResponse) {
        // Aborted by external timer
        NSLog(@"Dialog is aborted");
    }
}

- (IBAction)dialogOk:(id)sender
{
    // OK button is pushed
    [[NSApplication sharedApplication] 
            stopModalWithCode:DIALOG_OK];
}

- (IBAction)dialogCancel:(id)sender
{
    // Cancel button is pushed
    [[NSApplication sharedApplication] 
            stopModalWithCode:DIALOG_CANCEL];
}

上のサンプルコードには、3 つのメソッドがある。

まず showDialog: は、ダイアログを表示するためのメソッドだ。runModalForWindow: を呼び出して、ダイアログを表示している。このダイアログには、OK ボタンと Cancel ボタンがあって、押すと、それぞれ、dialogOk:dialogCancel: を呼び出すんだ。

それぞれのメソッドの中で、stopModalWithCode: を呼んでいる。OK のときは DIALOG_OK、キャンセルのときは DIALOG_CANCEL を引数として指定するんだ。これが呼ばれると、showDialog: の中の runModalForWindow: が戻ってくる。指定した引数が、返り値として得られるんだ。これにより、それぞれの返り血に応じた処理ができるわけだ。

■サンプルダウンロード:
Dialog.tar.gz

Application Kit - NSApplication

ドキュメント・モーダルダイアログ(シート)を表示する

Keywords: beginSheet

ドキュメント・モーダルダイアログは、Mac OS X で新しく導入されたダイアログだ。いわゆるシートダイアログってやつね。とにかく見た目が強力だったので、そっちばっかり強調されてるけど、それ以外にもいろいろおもしろいやつだ。

ドキュメント・モーダルダイアログとは、あるドキュメント、つまり、ある一つのウィンドウだけに関係するんダイアログなんだ。その状態をシートはたくみに表している。シートはウィンドウのタイトルバーから、にょろにょろっと出てきて、ウィンドウにくっついている。だから、シートと関連づけられているウィンドウは、とても明確に分かるんだ。さらに、ウィンドウを動かせばシートも動くし、後ろに持っていけばダイアログもいっしょに後ろに行く。他のウィンドウを選択することもできる。というわけで、かなりすぐれもののユーザインタフェースだと思う。

そんなシートを表示するには beginSheet:modalForWindow:modalDelegate:didEndSelector:contextInfo: を使う。

Application Kit/NSApplication.h

- (void)beginSheet:(NSWindow *)sheet 
        modalForWindow:(NSWindow *)docWindow 
        modalDelegate:(id)modalDelegate 
        didEndSelector:(SEL)didEndSelector 
        contextInfo:(void *)contextInfo;

このメソッドは、runModalForWindow: と違って非同期なんだ。つまり、これを呼ぶと、ダイアログを表示して、すぐ戻ってくる。ダイアログを閉じてないとしてもだ。そして、おおもとのイベントループで動くんだ。

シートダイアログを閉じるには、endSheet: を使う。実は、シートを表示させるときにセレクタを渡しているんだけど、endSheet: を呼ぶと、そのセレクタが呼び出されるんだ。そのメソッドの中で、ダイアログを閉じた後の処理を行う。

Application Kit/NSApplication.h

- (void)endSheet:(NSWindow *)sheet;
- (void)endSheet:(NSWindow *)sheet returnCode:(int)returnCode;

じゃ、サンプル。

Dialog/AppDelegate.m (sample)

- (IBAction)showSheet:(id)sender
{
    // Display sheet dialog
    [[NSApplication sharedApplication] 
        beginSheet:_sheetDialog 
                modalForWindow:_window 
                modalDelegate:self 
                didEndSelector:@selector(
                    sheetDidEnd:returnCode:contextInfo:) 
                contextInfo:nil];
}

- (void)sheetDidEnd:(NSWindow*)sheet 
                returnCode:(int)returnCode 
                contextInfo:(void*)contextInfo
{
    [_sheetDialog orderOut:self];
    
    // Check return code
    if(returnCode == DIALOG_CANCEL) {
        // Cancel button was pushed
        NSLog(@"Sheet is canceled");
        return;
    }
    else if(returnCode == DIALOG_OK) {
        // OK button was pushed
        NSLog(@"Sheet is accepted");
    }
}

- (IBAction)sheetOk:(id)sender
{
    // OK button is pushed
    [[NSApplication sharedApplication] 
        endSheet:_sheetDialog returnCode:DIALOG_OK];
}

- (IBAction)sheetCancel:(id)sender
{
    // Cancel button is pushed
    [[NSApplication sharedApplication] 
        endSheet:_sheetDialog returnCode:DIALOG_CANCEL];
}

このサンプルでは、showSheet: を呼び出すとシートを表示する。その際に、sheetDidEnd:returnCode:contextInfo: を閉じたあとに呼び出されるセレクタとして設定している。sheetOk:sheetCancel: を呼び出すと、ダイアログを閉じて登録されているメソッドが呼び出されるんだ。

■サンプルダウンロード:
Dialog.tar.gz

Application Kit - NSApplication

2 つのダイアログの違い

Keywords: application-modal dialog, document-modal dialog

アプリケーション・モーダルダイアログと、ドキュメント・モーダルダイアログ(シート)の違いをまとめてみよう。

プログラミング的な立場からいうと、アプリケーション・モーダルダイアログは、システムから制御を奪ってしまう。Aqua Human Inteface Guidelines の表現を借りると、システムを“ハイジャック”してしまうんだ。だから、アプリケーションがきちんとめんどうをみてやらないといけないし、不測の事態(よーするにフリーズね)も起こりやすい。

それに対して、シートは比較的安全。でも、ダイアログの表示から終了後の処理を、連続して書くことができない。ダイアログを表示させるところと、ダイアログが閉じた後の処理が、別のメソッドになってしまうんだ。これが、プログラム組むときめんどくさいんだよねー。

次にユーザインタフェース的な立場で考えてみよう。これまた Aqua Human Interface Guidlines によると、シートを使うのは次のような場合。

  • ある特定のドキュメントに対するダイアログの場合
  • シングルウィンドウのアプリケーションの場合

じゃ逆にシートを使わないのは?次のような場合。

  • 複数のウィンドウに対するダイアログの場合
  • ードレスウィンドウとして使いたいとき。パレットとか
  • ウィンドウにタイトルバーがないとき

だ、そうな。

Application Kit - NSApplication

簡単にダイアログを表示する

Keywords: NSRunAlertPanel(), NSBeginSheet()

runForModalDialog: や、beginSheet:modalForWindow:modalDelegate:didEndSelector:contextInfo: を使ってダイアログを表示するときは、.nib を用意してやらないといけない。たんにアラートを出したいときとかは、めんどくさいよね。というわけで、関数呼び出し一発で表示させるものが用意されている。NSRunAlertPanel()NSBeginAlerSheet() だ。

Application Kit/NSPanel.h

APPKIT_EXTERN int NSRunAlertPanel(
        NSString *title, 
        NSString *msg, 
        NSString *defaultButton, 
        NSString *alternateButton, 
        NSString *otherButton, 
        ...);
APPKIT_EXTERN void NSBeginAlertSheet(
        NSString *title, 
        NSString *defaultButton, 
        NSString *alternateButton, 
        NSString *otherButton, 
        NSWindow *docWindow, 
        id modalDelegate, 
        SEL didEndSelector, 
        SEL didDismissSelector, 
        void *contextInfo, 
        NSString *msg, 
        ...);

NSRunAlertPanel() は、アプリケーション・モーダルダイアログを表示させるんだ。最高で 3 つのボタンを持つ。defaultButtonalternateButtonotherButton に表示させたい文字を指定するんだ。nil を渡すと、ボタンは表示されない。

NSBeginAlertSheet() は、シートダイアログを表示ね。NSRunAlertPanel() と違うのは、関連する NSWindow を指定するのと、セレクタを指定すること。

使ってみると、こんな感じかな。

Dialog/AppDelegate.m (sample)

- (IBAction)runAlertPanel:(id)sender
{
    int	result;
    
    // Display modal dialog
    result = NSRunAlertPanel(
                @"Normal Alert", 
                @"Normal Alert", 
                @"OK", 
                @"Cancel", 
                @"Huh?");
    
    if(result == NSAlertDefaultReturn) {
        // OK button was pushed
        NSLog(@"Dialog is accepted");
    }
    else if(result == NSAlertAlternateReturn) {
        // Cancel button was pushed
        NSLog(@"Dialog is canceled");
    }
    else if(result == NSAlertOtherReturn) {
        // Huh? button was pushed
        NSLog(@",,, do you have any questions?");
    }
}

- (IBAction)runAlertSheet:(id)sender
{
    // Display sheet dialog
    NSBeginAlertSheet(
            @"Alert sheet", 
            @"OK", 
            @"Cancel", 
            @"Huh?", 
            _window, 
            self, 
            @selector(
                runAlertSheetDidEnd:returnCode:contextInfo:), 
            nil, 
            nil, 
            @"Alert sheet");
}

■サンプルダウンロード:
Dialog.tar.gz

Application Kit - NSApplication

NSApplication のサブクラスを使う

Keywords: Cocoa own configuration

実は NSApplication のサブクラスを作る必要性ってのは低い。なぜなら NSApplication の機能を拡張したいときは、デリゲートを使えばたいていのことはできちゃうからだ。でも、ときにはサブクラスを作りたいときもある。

そんなときは素直に NSApplication のサブクラスを作ろう。サブクラス自体は Interface Builder とかで作ってやれば、すぐできる。ただ、そのサブクラスをアプリケーションに使わせるには、次の設定を忘れてはいけないんだ。

  1. MainMenu.nib の File's owner として指定する
  2. ターゲットの「Cocoa 固有の設定」のところで、主要クラスとして指定する

これで新しく作ったサブクラスが NSApplication の代わりに使われるぜ。

back to top content

Copyright © 2002-2006 HMDT. All rights reserved.