2010年5月20日木曜日

三角形の描画

Open GLで三角形を描画するには、glVertexPointer関数、glColorPointer関数、glEnableClientState関数、glDrawArrays関数を使用します。


void glVertexPointer( (GLint)頂点1つあたりの配列の要素数, (GLenum)頂点配列のタイプ, 配列の開始位置, 配列へのポインタ);
最初に頂点の座標を格納した配列をOpen GLに登録します。


void glEnableClientState( GLenum );
頂点を登録したことをOpen GLに通知します。
引数には、登録した配列のタイプを指定します。


void glColorPointer( (GLint)頂点1つあたりの配列の要素数, (GLenum)頂点配列のタイプ, 配列の開始位置, 配列へのポインタ);
頂点の色を登録します。


void glDrawArrays( (GLenum)描画モード, (GLint)描画を開始する配列のインデックス, (GLint) 描画する頂点の数);
描画します。


    const GLfloat triangleVerteces[] = {
        -0.5f, -0.5f,
         0.5f, -0.5f,
         0.0f,  0.5f,
    };
    const GLubyte triangleColors[] = {
        255, 0, 0, 255,
        255, 0, 0, 255,
        255, 0, 0, 255,
    };
   
    // 頂点座標を登録
    glVertexPointer(2, GL_FLOAT, 0, triangleVerteces);
    glEnableClientState(GL_VERTEX_ARRAY);
   
    // 頂点色を登録
    glColorPointer(4, GL_UNSIGNED_BYTE, 0, triangleColors);
    glEnableClientState(GL_COLOR_ARRAY);
   
    // 描画
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 3);

上の例では、赤い三角形が1つ描画されます。
頂点座標の配列(triangleVertices)にX, Y座標のみを与え、Z座標は省きました。このため、glVertexPointer関数の第一引数(頂点1つあたりの配列の要素数)は2(X座標とY座標の2つ)としています。
また、triangleColors配列にはR, G, B, Aの色(Aはアルファ値)を保持しています。
後は、配列の型(GL_FLOAT, GL_UNSIGNED_BYTE等)と配列の開始インデックス(0)、配列アドレスを与えてglVertexPointer,glColorPointerを呼び出します。
glEnableClientState関数には、それぞれ頂点座標を与えるときにはGL_VERTEX_ARRAYを、色配列を与える時はGL_COLOR_ARRAYを与えます。
最後にglDrawArrayで配列に指定した座標と色で描画します。
第1引数の描画モードとは、頂点座標をつなげて描画を行うか(GL_TRIANGLE_STRIP)、ばらばらの三角形として描画を行うか(GL_TRIANGLES)、線分として描画を行う(GL_LINES)かの描画モードを指定します。
例えば、座標が4つ指定してある場合、GL_TRIANGLE_STRIPが指定してあると0番目〜2番目までの頂点を使って三角形を描画し、1番目〜3番目までの頂点を使って三角形を描画するというように、次の頂点と前の三角形とを順次つなげながら描画を行います。
GL_TRIANGLESが指定してあると、0番目〜2番目までの頂点を使って三角形を描画し、3番目〜5番目までの頂点を使って三角形を描画します。
GL_LINESが指定してある場合は、0〜1番目の頂点をつなぐ線分、2〜3番目をつなぐ線分というように、ばらばらの線が描画されます。

2010年5月19日水曜日

Open GLの座標

Open GLでは座標の原点が通常のアプリケーションのように左上隅ではなく、画面の中央にあります。
画面の中央を(0, 0) として、画面左側がX座標マイナス。右側がプラス。中央より上がY座標プラス。下側がマイナスという座標体系のようです。

しかもこの座標体系は、画面の縦横ピクセルが異なる(320 x 480)のに1:1の比率になっているようです。
このため例えば正方形を描画すると、縦の画素数が多いのに横の画素数と同じと見なされて縦につぶれたような長方形が描かれるようになります。

ただし、この座標体系は関数を呼び出すことで変更することが可能です。

glOrthof( Xの左端指定, Xの右端指定, Yの下端指定, Yの上端指定, Zの手前指定, Zの奥指定);


ES1Renderer/ES2RendererクラスのrenderメソッドがOpen GLを初期化するメソッドですが、この中にある座標体系を初期化する部分を以下のようにします。


glMatrixMode(GL_PROJECTION);
glLoadIdentity();

// ここで座標体系を指定する
glOrthof( -1.0f, 1.0f, -1.0f, 1.0f, 0.5f, -0.5f );
・
・
・

これで、画面に円を描画しても縦につぶれて描画されなくなります。

また、通常のアプリケーションのように、左上隅を原点(0, 0)、右下隅を(320, 480)としたい場合は、以下のようにします。

glOrthof( 0.0f, 320.0f, 480.0f, 0.0f, 50.0f, -50.0f );

Z軸は奥行きなのでここではあまり気にしないでください。(汗)

Open GLのファイル構成

xcodeでOpen GLテンプレートを使ってプロジェクトを作成すると、プロジェクト名Delegate.mの他に、EAGLViewのソースとヘッダ、その他、ESRenderer/ES1Renderer  /ES2Rendererのソースとヘッダがそれぞれ作成されます。
EAGLViewがOpen GLを表示する部分で、ESRendererがES1RendererとES2Rendererをスーパークラスとしてラップするクラス、ES1Rendererが 初代のiPhone 3G/iPodTouch1,2世代機で使用可能なOpen GLの描画クラス、ES2RendererがiPhone 3GS/iPodTouch3世代名以降で使用可能な描画クラスだそうです。

ES2Rendererクラスを使うと機種が限定されるみたいなので注意が必要です。

OpenGLに挑戦

iPhone OSはOpen GLをサポートしています。
Open GLを使えばハードウェアのグラフィック機能をフルに生かせます。
今回からはちょっとOpen GLにも挑戦してみようと思います。
Open GL自体がほとんど初めてのようなものなので間違いがあるかもしれませんが、温かく見守っていただければと思います。

ローカライズ

アプリケーションをローカライズするには、xcode上でResourceの所にstringsファイルを追加し、ファイル名を「Localizable.strings」にします。
ファイル名は「Localizable.strings」でないと機能しないので注意が必要です。
そして、xcode上でこのファイルの「情報を見る」を選択し、「ファイルをローカライズ可能にする」ボタンを押します。
ローカリゼーションの所には最初は英語のみ表示されているので、「ローカリゼーションを追加」 ボタンでローカライズしたい言語を追加します。
xcodeの左側のLocalizable.strings配下にEnglishと、追加した言語のファイルが追加されていますので、最後にファイルタイプを「UTF-16」にして、ファイルに以下のように記述します。


"MESSAGE" = "日本語のメッセージ";
"CANCEL" = "キャンセル";

=の左側は後からローカライズされたメッセージを読み出す時のキーになるものです。
右側がローカライズされたメッセージです。上の例では日本語(Japanese)のローカライズメッセージを設定しています。Englishの方のファイルにも同様に英語のメッセージを設定します。

"MESSAGE" = "English Message";
"CANCEL" = "Cancel";


上で設定したローカライズされたメッセージをアプリケーションから取得するには、以下のようにします。


    // リソースファイルからローカライズ用テキストを取得
    NSString* strMsg = NSLocalizedString( @"MESSAGE", nil );
    NSString* strCancel = NSLocalizedString( @"CANCEL", nil );


上記で取得されたメッセージをラベルやボタン等に設定すれば、iPhoneのシステム言語設定に従ったメッセージを表示することが可能です。



さらに、iPhoneのホーム画面に表示されるアプリケーション名もローカライズすることが可能です。
「Localizable.strings」ファイルと同じやり方で「InfoPlist.strings」ファイルを作成し、ローカライズ可能にし、ファイルタイプ(UTF16)の設定を行います。
そして、以下のように記述します。

CFBundleDisplayName = "日本語のアプリ名";

こちらも日本語(Japanese)と英語(English)両方のアプリケーション名を設定します。
これだけで、iPhoneのホーム画面に表示されるアプリケーション名がiPhoneのシステム言語設定によって変化するようになります。

2010年5月18日火曜日

設定をファイルに保存

アプリケーションの設定をファイルに保存するには、NSUserDefaultsクラスを使用します。


    // NSUserDefaultsのインスタンス取得
    NSUserDefaults* defaults = [ NSUserDefaults standardUserDefaults ];
   
    // 設定された文字列を登録
    [ defaults setObject: @"設定値" forKey: @"SET1" ];
  
    // 設定された数値を登録
    [ defaults setInteger: 100 forKey: @"SET2" ];


standardUserDefaultsメソッドでNSUserDefaultsクラスのインスタンスを生成し、setObjectやsetIntegerメソッドでそれぞれの設定値を登録します。1番目の引数が保存する値で、2番目の引数が後で読み出す時にキーとなる値です。

保存ファイルは、アプリケーションをインストールしたフォルダのLibrary/Preferencesフォルダに「com.yourcompany.アプリケーション名.plist」というファイル名で保存されます。中身はXMLファイルです。


設定ファイルを読み出す時は、アプリケーション起動後のawakeFromNibメソッド等で以下のようにします。

    // デフォルト値を作成
    NSDictionary* defDic = [ NSDictionary dictionaryWithObjectsAndKeys:
                            @"設定値デフォルト", @"SET1"
                            , [ NSNumber numberWithInt: 100 ], @"SET2"
                            , nil ];
   
    // NSUserDefaultsのインスタンス取得と読み込み
    NSUserDefaults* defaults = [ NSUserDefaults standardUserDefaults ];
   
    // デフォルト値をNSUserDefaultsに設定
    [ defaults registerDefaults:defDic ];
   
    // 読み込んだ設定値のテキストを取得
    NSString* strSet1 = [ defaults stringForKey: @"SET1" ];
   
    // 読み込んだ設定値の数値を取得
    NSInteger nSet2 = [ defaults integerForKey: @"SET2" ];


NSDictionaryというのは、アプリケーションの初回起動時には設定ファイルが存在しないので、その場合のデフォルト値を設定する為に使用しています。上記の例では、SET1というキーに設定値デフォルト、SET2には数値の100をデフォルト値として指定しています。
その後、NSUserDefaultsクラスのregisterDefaultsメソッドでNSDictionaryを設定しています。

後は、SET1のキーに該当する値は文字列なので、文字列を読み出すstringForKeyメソッド、SET2のキーに該当する値は数値なので、数値を読み出すintegerForKeyメソッドを呼出します。これで、デフォルト値か、前回保存した設定値を読み出すことができます。

2010年5月17日月曜日

ちょっとした情報を表示するUIAlertView

ちょっとした情報を表示するにはUIAlertViewを使用します。
ほとんどUIActionSheetと同じですが、もう少し詳細な情報を表示できるのが異なる点です。


- (id) initWithTitle: (NSString*) title
    message: (NSString*) message
    delegate: (id) delegate
    cancelButtonTitle: (NSString*) cancelButtonTitle
    otherButtonTitle: (NSString*) otherButtonTitle

titleにはメッセージのタイトルを指定します。無い場合はnilを指定します。
messageにはメッセージの内容を指定します。長いメッセージを指定した場合はスクロールバー等も自動的に表示してくれます。無い場合はnilを指定します。
delegateにはボタンが押された時等に呼出されるdelegateを実装しているクラスを指定します。指定したクラスにはプロトコルを宣言する必要があります。無い場合はnilを指定します。
cancelButtonTitleには、キャンセルボタンに表示するタイトルを指定します。キャンセルボタンが無い場合はnilを指定することも可能です。
otherButtonTitleには、その他のボタンに表示するタイトルを指定します。無い場合はnilを指定します。


ボタンが押された場合は、以下のdelegateが呼出されます。

- (void) alertView: (UIAlertView*) alertView clickedButtonAtIndex: (NSInteger) buttonIndex
buttonIndexには、0がキャンセルボタン。1以降はotherButtonTitleで指定したボタンのインデックスが入っています。

UIActionSheetクラス

UIActionSheetクラスは、重要なデータを消去したりする場合に、「本当に消去してよいですか?」等の様に確認の為に使用されるコントロールです。
WindowsでいうところのMessageBoxの様なものでしょうか?

UIActionSheetクラスのインスタンスの初期化には「initWithTitle」メソッドを使います。

- (id) initWithTitle: (NSString*) title
        delegate: (id < UIActionSheetDelegate>) delegate
        cancelButtonTitle: (NSString*) cancelButtonTitle
        destructiveButtonTitle: (NSString*) destructiveButtonTitle
        otherButtonTitle: (NSString*) otherButtonTitle1, otherButtonTitle2, ・・・

titleにはユーザーに問い合わせるメッセージを指定します。メッセージを表示しない時はnilを指定します。
delegateには、UIActionSheetクラスがdelegateで呼出すクラスを指定します。指定されたクラスは、UIActionSheetDelegateプロトコルを宣言する必要があります。
cancelButtonTitleには、キャンセルボタンに表示するメッセージを指定します。メッセージを表示しない時はnilを指定します。
destructiveButtonTitleには、消去をOKしたりするような時のボタンに表示するメッセージを指定します。メッセージを表示しない時はnilを指定します。
otherButtonTitleには、その他のボタンを表示する必要がある場合に指定します。カンマで区切って複数指定した場合は複数のボタンが表示されるようになります。このボタンが不要の場合はnilを指定します。


UIActionSheetコントロールのメッセージを表示する時は、UIActionSheetクラスのshowInViewメソッドを使います。

- (void) showInView: (UIView*)view
UIActionメッセージを表示する対象のUIViewクラスを指定します。


UIActionSheet上のボタンが押されたら、初期化メソッドで指定したクラスの以下のデリゲートが呼出されます。

- (void) actionSheet: (UIActionSheet*) actionSheet clickedButtonAtIndex: (NSInteger) buttonIndex
buttonIndexには、初期化メソッドで指定したボタンのインデックスが入ります。
インデックス順序は0がdestructiveButtonTitleで指定したボタン。1がotherButtonTitle1,2,3・・(指定した場合)と続き、最後のインデックスがcancelButtonTitleで指定したボタンになります。


以下がUIActionSheetの例です。

    // アクションシートを生成
    UIActionSheet* actSheet = [ [ UIActionSheet alloc ]
                                initWithTitle:@"Are you sure?"
                                delegate:self
                                cancelButtonTitle:@"CANCEL"
                                destructiveButtonTitle:@"OK"
                                otherButtonTitles:nil ];
   
    // アクションシートにタグを設定
    actSheet.tag = 1;
   
    // アクションシートを表示
    [ actSheet showInView: imageView ];
   
    // アクションシートは表示直後に解放してかまわない
    [ actSheet release ];


アクションシートは表示したらiPhone OS内部にメモリが確保されるそうなので、すぐにreleaseして構いません。

UIActionSheetを複数表示した場合、どのUIViewから呼出されたUIActionSheetか分からなくなります。この場合、どのアクションシートかを判定する為にtagプロパティに識別子を格納しておきます。

- (void) actionSheet: (UIActionSheet*) actionSheet clickedButtonAtIndex: (NSInteger) buttonIndex
{
    if ( actionSheet.tag == 1 )
    { // UIView1で生成したアクションシートから呼出されたdelegate
        if ( buttonIndex == 0 )

        { // OKボタン押下

            処理
        }
        if ( buttonIndex == 1 )
        { // キャンセルボタン押下
            処理
        }
    }
    else if ( actionSheet.tag == 2 )
    { // UIView2で生成したアクションシートから呼出されたdelegate
        if ( buttonIndex == 0 )
        { // OKボタン押下
            処理
        }
        if ( buttonIndex == 1 )
        { // キャンセルボタン押下
            処理
        }
    }
}

メニューから選択させるPickerView

メニューに複数項目を表示し、その中からユーザに選択させる為のコントロールとして、UIPickerViewがあります。
UIPickerViewクラスには複数の「行」と「列」を表示可能です。
この「行」と「列」のことを、UIPickerViewクラスではそれぞれ「Row」と「Component」と呼んでいます。

また、UIPickerViewクラスが要素の行数と列数がいくつあるのかをアプリケーションに対して問い合わせる時や、特定の行列が選択された時にアプリケーションに対して通知してくる等のイベントをデリゲートを経由して行います。


- (NSInteger) numberOfComponentsInPickerView: (UIPickerView*) view
component(列)がいくつあるのかの問い合わせです。
アプリケーションは要素の列数(component)の数を返却します。


- (CGFloat) pickerView: (UIPickerView*) view widthForComponent: (NSUInteger) comp
特定のcomponent(列)の幅が何ピクセルあるかの問い合わせです。
アプリケーションはcomponent(列)の幅をCGFloat型で返却します。


- (NSUInteger) pickerView: (UIPickerView*) view numberOfRowsInComponent:(NSUInteger) comp
特定のcomponent(列)の中に何個のRow(行)が存在するかの問い合わせです。
アプリケーションは要素の行数(row)を返却します。


- (NSString*) pickerView: (UIPickerView*) view titleForRow: (NSInteger) row forComponent: (NSInteger) component
特定の行と列に表示する文字列の問い合わせです。
アプリケーションはパラメータで指定された行と列に該当する文字列を返却します。


- (void) pickerView: (UIPickerView*) view didSelectRow: (NSInteger) row inComponent: (NSInteger) component
特定の行列が選択されたことの通知です。
アプリケーションは選択された行列を処理します。



途中で表示する行と列のデータを変更する場合は、UIPickerViewクラスのreloadComponentメソッドかreloadAllComponentsメソッドを呼びます。


- (void) reloadComponent: (NSInteger) comp
特定component(列)が変更されたことをアプリケーションからUIPickerViewクラスに通知します。
compに列番号を指定します。
このメソッドが呼ばれると、UIPickerViewクラスのインスタンスは上記のメソッドの上から4つ(numberOfComponentsInPickerView〜pickerView: titleForRow: rowForComponent)が順番に呼出されて表示が更新されます。


- (void) reloadAllComponents
全てのcomponentが変更されたことをアプリケーションからUIPickerViewクラスに通知します。
このメソッドが呼ばれると、UIPickerViewクラスのインスタンスは上記のメソッドの上から4つ (numberOfComponentsInPickerView〜pickerView: titleForRow: rowForComponent)が順番に呼出されて表示が更新されます。

テキストボックス

テキストを入力する為のコントロールにはUITextFieldがあります。これは1行しか入力できないコントロールなのでURL等を指定する時に使います。
複数の行を入力させるにはUITextViewを使います。
メモ帳のようなアプリケーションではUITextViewを使います。

UITextFieldは1行の入力なので、仮想キーボード上でEnterキーを押せば編集が完了したことが分かりますが、UITextViewのように複数行を編集させるコントロールの場合、Enterキーは改行を意味することになります。
そのため、編集の開始、終了等を明確にする為にdelegateを使ってアプリケーションに通知する仕組みが備わっています。


- (BOOL) textViewShouldBeginEditing: (UITextView*) textView
(戻り値) BOOL YES or NO
編集を開始しても良いかの問い合わせ
アプリケーション側で都合が悪ければ戻り値にNOを返却します。


- (void) textViewDidBeginEditing: (UITextView*) textView
編集が開始されたことを通知
このデリゲート上で編集終了用のボタンを表示したりします。


- (void) textViewDidChangeSelection: (UITextView*) textView
編集中のテキストが選択された時及びカーソル位置が変更された時に呼ばれます。
これが呼出された時にtextViewのselectedRangeプロパティを取得すると、NSRange型の選択範囲の情報が取得できます。NSRange型はカーソルの位置(location)と長さ(length)をNSUInteger型で格納する構造体です。
カーソル位置が変更されただけの場合はlengthには0が入ります。


- (BOOL) textView: (UITextView*) textView shouldChangeTextInRange: (NSRange) range replacementText: (NSString*) text
(戻り値)BOOL YES or NO
テキストを変更しても良いかの事前通知です。
rangeに変更対象の位置(location)と長さ(length)が入り、textに変更後の文字列が入っています。
このデリゲートは選択範囲の置き換えだけでなく、1文字入力や、文字削除の場合も呼出されます。
1文字入力の場合はrangeの長さ(length)が0になっています。また、文字削除の場合はrangeの位置(location)と長さ(length)に削除範囲が設定され、textに空の文字列が設定されることになります。
テキスト入力を許可する場合はYESを返却し、アプリケーション側で許可したくない場合はNOを返却します。


- (void) textViewDidChange: (UITextView*) textView
テキストが変更されたことの通知です。


- (BOOL) textViewShouldEndEditing: (UITextView*) textView
(戻り値)BOOL YES or NO
編集を終了しても良いかの問い合わせです。
アプリケーション側で編集を終わらせたくない場合にはNOを返却しますが、NOを返却するとフォーカスが他のコントロールに移動できないことになるので注意が必要です。


- (void) textViewDidEndEditing: (UITextView*) textView
編集が終了したことを通知します。



最後の2つのデリゲート(textViewShouldEndEditing/textViewDidEndEditing)は少し注意が必要です。

UITextViewが複数ある場合は別のUITextViewコントロールをタップすれば上記2つのデリゲートを呼出してくれますが、UITextViewコントロールが1つしか無い場合、ユーザーが編集を終わらせるきっかけがなくなってしまうことになります。(編集を終わらせるボタンが無い為)
このため、編集を終わらせる為にUIBarButtonItem等が押された時にUITextViewに編集終了を通知する必要があります。
この為のメソッドがUIResponderクラスにある「resignFirstResponder」メソッドです。
UITextViewクラスはUIResponderクラスを継承しているのでこのメソッドを呼出すことが可能です。

ボタンが押された時のイベントとしてresignFirstResponderを呼出してあげれば、UITextViewが編集の終了を検知してtextViewShouldEndEditing/textViewDidEndEditingのデリゲートを呼出してくれることになります。


- (void) endEditButtonClick
{
    //  UITextViewクラスに編集の終了を通知
    [ textView resignFirstResponder ];
}