ホームページ開発ツール>Xojo / Real Studio Trial and Error・CocoaのDeclareでツールバーを実装する

 Xojo / Real Studio Trial and Error

CocoaのDeclareでツールバーを実装する

目次
 はじめに

 以下は、Xojo Cocoaビルドについての話題です。

 Xojoは標準でツールバーを用意していますが、以下の制約があります。
 その点、Cocoaのツールバーは全て対応しています。なので、使えるか、調べてみました。

 なお検証には、Xojo 2014 Release 3.1を用いています。(Mac mini mid 2010 + OS X 10.10.5 Yosemite)


 方針

 実装は、ツールバーと、その上に置くツールバーアイテム(以下、アイテム)の双方それぞれについて行います。

 まずはツールバーですが、アイテムの登録はDelegateメソッドで行う必要があります。
 そのため、以前の通知センターの時と同じく、Objective-CのランタイムAPIを用いた動的クラス生成を用いることとしました。
 今回は、以下のDelegateメソッドを対象とします。
 - toolbarDefaultItemIdentifiers:(デフォルトのアイテムの並びリストを作成)
 - toolbarAllowedItemIdentifiers:(カスタマイズダイアログに表示されるアイテムの並びリストを作成)
 - toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:(ユーザ定義アイテムの生成)

 次にアイテムですが、こちらはDelegateは不要ですが、Target/Actionの設定が必要となります。
 例によってmacoslibを紐解いてみたところ、基本的にはDelegateと同じ、動的クラス生成を使えばいいことが分かりました。
 ただし、Delegateとは異なり、登録するのはフレームワークから提供される予約語ではなく、自分で定義(以下の実装では「action:」としている)したものになります。


 共有メソッドについて

 Xojoの共有メソッド(Shared Methods)は、Objective-Cでいうクラスメソッドのことのようです。
 macoslibは動的クラス生成を、共有メソッドで実装しています。
 当初は汎用化が目的かと思っていましたが、クラスを作るのはクラスメソッドで、と言われればそうかなという気もします。

 また、実験してみて分かったことですが、ツールバーのDelegateメソッドを通常のメソッドで実装すると、起動時にエラーが発生してしまいます。
今回のケースでエラーが発生するのはtoolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:メソッドのみで、他は問題ありません。
どうもメソッドかオブジェクトが作成前に参照されているような感触です。(デバッガごと落ちてしまうので詳細不明。なので、あくまで感触です。)
 一方、共有メソッドで実装すれば、問題なく動作します。
前回の通知センターでは通常のメソッドで実装しても特に問題はありませんでしたが、これはたまたま動作した、と考えるべきかもしれません。
 このようなことから、以下の実装では、動的クラス生成に共有メソッドを使っています。
追記:登録したXojo側のメソッドには、2個のPtr型引数を先頭に追加する必要があります。前回は引数を参照しなかったので、なくても実害はありませんでしたが、今回は参照しますので、忘れないようにします。
参考サイト(3):Objective-C - Blockで動的にメソッドを実装できるクラスの作り方 - Qiita

 Xojoでの実装

 今回は実験として、標準的なアイテム(カラー、フォント、プリント、スペース、伸縮自在のスペース)とユーザ定義アイテム(保存)を配置してみます。
 なお、実装にあたっては、以下のサイトを参考にさせて頂きました。
 参考サイト(1):Introduction to Toolbars
 参考サイト(2):NSToolbar basics | No pain no gain
  1. Xojoで新規プロジェクトを作成
  2. ユーザ定義アイテム用のアイコン画像ファイル(ここではsave.pngとしている)を、プロジェクトに追加
  3. 以下をWindow1のOpenイベントに記述
    // Shared MethodsからMethodsにアクセスするために、自身をインスタンスとして登録
    Window1Instance = self
    
    // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
    Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
    
    // ツールバーの初期化
    Dim toolbar1 As Ptr = NSClassFromString("NSToolbar")
    Declare Function toolbarAlloc Lib "Cocoa" Selector "alloc" (receiver As Ptr) As Ptr
    toolbar1 = toolbarAlloc(toolbar1)
    Declare Function toolbarInit Lib "Cocoa" Selector "initWithIdentifier:" (receiver As Ptr, identifier As CFStringRef) As Ptr
    toolbar1 = toolbarInit(toolbar1, "MySampleToolbar")
    
    // 各種パラメータの設定
    Declare Sub setAllowsUserCustomization Lib "Cocoa" Selector "setAllowsUserCustomization:" (receiver As Ptr, value As Boolean)
    setAllowsUserCustomization(toolbar1, true)
    Declare Sub setAutosavesConfiguration Lib "Cocoa" Selector "setAutosavesConfiguration:" (receiver As Ptr, value As Boolean)
    setAutosavesConfiguration(toolbar1, true)
    Declare Sub setDisplayMode Lib "Cocoa" Selector "setDisplayMode:" (receiver As Ptr, value As Integer)
    setDisplayMode(toolbar1, 1)  // 0=Default, 1=Icon & Text, 2=Icon Only, 3=Text Only
    
    // Delegateの設定
    Declare Sub setDelegate Lib "Cocoa" Selector "setDelegate:" (receiver As Ptr, id As Ptr)
    setDelegate toolbar1, makeDelegate()  // Delegateの設定
    
    // ウィンドウにツールバーを登録
    Declare Sub setToolbar Lib "Cocoa" Selector "setToolbar:" (receiver As WindowPtr, toolbar As Ptr)
    setToolbar(self, toolbar1)
    
    // 後処理
    Declare Sub release Lib "Cocoa" Selector "release" (toolbar As Ptr)
    release(toolbar1)
    
  4. 以下をWindow1のメソッドに追加
    メソッド名: ToolbarItemClicked
    引数: sender As Ptr
    
    // アイテムの種別がSaveなら必要な処理を行う
    Declare Function label Lib "Cocoa" Selector "label" (receiver As Ptr) As CFStringRef
    if label(sender) = "Save" then
        …
    end if
    
  5. 以下をWindow1の共有メソッド(Shared Methods)に追加
    メソッド名: actionEvent
    引数: id As Ptr, SEL As CString, sender As Ptr
    
    // Sharedでないメソッドに渡す
    Window1Instance.ToolbarItemClicked(sender)
    
    注)SELの型をPtrとしていたが、CStringの方が相応しいので変更した。(未使用なので変更しなくても実害はなし。)(2018.07.30)
  6. 以下をWindow1の共有メソッド(Shared Methods)に追加
    メソッド名: makeDelegate
    戻り値型: Ptr
    
    // 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。
    Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
    Declare Function NSSelectorFromString Lib "Cocoa" (aSelName As CFStringRef) As Ptr
      
    // Declare宣言
    Declare Function objc_allocateClassPair Lib "Cocoa" (superclass As Ptr, name As CString, extraBytes As Integer) as Ptr
    Declare Sub objc_registerClassPair Lib "Cocoa" (cls As Ptr)
    Declare Function class_addMethod Lib "Cocoa" (cls As Ptr, name As Ptr, imp As Ptr, types As CString) As Boolean
    
    // クラス名をmyNSToolbarDelegate(名前は任意。少なくとも今回のケースでは参照されない。)、メタクラス名をNSObjectにして、生成
    Dim newClassId As Ptr = objc_allocateClassPair(NSClassFromString("NSObject"), "myNSToolbarDelegate", 0)
    // ランタイムに登録(参照を可能とするため)
    objc_registerClassPair newClassId
    // Delegateの対象となるメソッドを追加(toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:をXojo側で用意したtoolbarItemForItemIdentifierWillBeInsertedIntoToolbarメソッドで受け取る。)
    if not class_addMethod (newClassId, NSSelectorFromString("toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:"), AddressOf toolbarItemForItemIdentifierWillBeInsertedIntoToolbar, "@@:@@c") then
        msgBox "error."
        return nil
    end if
    // Delegateの対象となるメソッドを追加(toolbarAllowedItemIdentifiers:をXojo側で用意したtoolbarAllowedItemIdentifiersメソッドで受け取る。)
    if not class_addMethod (newClassId, NSSelectorFromString("toolbarAllowedItemIdentifiers:"), AddressOf toolbarAllowedItemIdentifiers, "@@:@") then
        msgBox "error."
        return nil
    end if
    // Delegateの対象となるメソッドを追加(toolbarDefaultItemIdentifiers:をXojo側で用意したtoolbarDefaultItemIdentifiersメソッドで受け取る。)
    if not class_addMethod (newClassId, NSSelectorFromString("toolbarDefaultItemIdentifiers:"), AddressOf toolbarDefaultItemIdentifiers, "@@:@") then
        msgBox "error."
        return nil
    end if
    // Delegateの対象となるメソッドを追加(toolbarSelectableItemIdentifiers:をXojo側で用意したtoolbarSelectableItemIdentifiersメソッドで受け取る。)
    if not class_addMethod (newClassId, NSSelectorFromString("toolbarSelectableItemIdentifiers:"), AddressOf toolbarSelectableItemIdentifiers, "@@:@") then
        msgBox "error."
        return nil
    end if
    
    // 上記で生成したクラスのインスタンスを作成
    Declare Function alloc Lib "Cocoa" selector "alloc" (class_id As Ptr) As Ptr
    Declare Function init Lib "Cocoa" selector "init" (obj_id As Ptr) As Ptr
    Dim delegateId As Ptr = init(alloc(newClassId))
    
    // インスタンスを返す
    return delegateId
    
  7. 以下をWindow1の共有メソッド(Shared Methods)に追加
    メソッド名: makeTarget
    引数: name As String
    戻り値型: Ptr
    
    // 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。
    Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
    Declare Function NSSelectorFromString Lib "Cocoa" (aSelName As CFStringRef) As Ptr
    
    // Declare宣言
    Declare Function objc_allocateClassPair Lib "Cocoa" (superclass As Ptr, name As CString, extraBytes As Integer) as Ptr
    Declare Sub objc_registerClassPair Lib "Cocoa" (cls As Ptr)
    Declare Function class_addMethod Lib "Cocoa" (cls As Ptr, name As Ptr, imp As Ptr, types As CString) As Boolean
    
    // 既にインスタンス作成済なら、それを返す
    if TargetInstance <> nil then
        return TargetInstance
    end if
    
    // クラス名を引数のname(名前は任意だが、重複を避けるためにアイテムのIdentifierにしている。)、メタクラス名をNSObjectにして、生成
    Dim newClassId As Ptr = objc_allocateClassPair(NSClassFromString("NSObject"), name, 0)
    // ランタイムに登録(参照を可能とするため)
    objc_registerClassPair newClassId
    // Tarrgetに送られてきたActionの受け口となるメソッドを追加(action:をXojo側で用意したactionEventメソッドで受け取る。)
    if not class_addMethod (newClassId, NSSelectorFromString("action:"), AddressOf actionEvent, "@@:@") then
        msgBox "error."
        return nil
    end if
    
    // 上記で生成したクラスのインスタンスを作成
    Declare Function alloc Lib "Cocoa" selector "alloc" (class_id As Ptr) As Ptr
    Declare Function init Lib "Cocoa" selector "init" (obj_id As Ptr) As Ptr
    Dim targetId As Ptr = init(alloc(newClassId))
    
    // インスタンスを保持
    TargetInstance = targetId
    
    // インスタンスを返す
    return targetId
    
    注)初出時、既に生成済ならクラスを返すようになっていましたが、明らかに間違いで、生成済のインスタンスを返すようにしました。

  8. 以下をWindow1の共有メソッド(Shared Methods)に追加
    メソッド名: toolbarAllowedItemIdentifiers
    引数: id As Ptr, SEL As CString, toolbar As Ptr
    戻り値型: Ptr
    
    // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
    Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
    
    Dim arr As Ptr = NSClassFromString("NSMutableArray")  // クラスメソッドなので、まずNSMutableArrayクラスを取得
    Declare Function getArray Lib "Cocoa" Selector "array" (receiver As Ptr) As Ptr  // Return Array*
    arr=getArray(arr)
    
    Declare Sub addObject Lib "Cocoa" Selector "addObject:" (receiver As Ptr, obj As CFStringRef)
    addObject(arr, SaveToolbarItemIdentifier)  // ユーザ定義アイテム
    addObject(arr, "NSToolbarShowColorsItem")
    addObject(arr, "NSToolbarShowFontsItem")
    addObject(arr, "NSToolbarPrintItem")
    addObject(arr, "NSToolbarSpaceItem")
    addObject(arr, "NSToolbarFlexibleSpaceItem")
      
    // リストを返す
    return arr
    
    注)SELの型をPtrとしていたが、CStringの方が相応しいので変更した。(未使用なので変更しなくても実害はなし。)(2018.07.30)
  9. 以下をWindow1の共有メソッド(Shared Methods)に追加
    メソッド名: toolbarDefaultItemIdentifiers
    引数: id As Ptr, SEL As CString, toolbar As Ptr
    戻り値型: Ptr
    
    // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
    Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
    
    Dim arr As Ptr = NSClassFromString("NSMutableArray")  // クラスメソッドなので、まずNSMutableArrayクラスを取得
    Declare Function getArray Lib "Cocoa" Selector "array" (receiver As Ptr) As Ptr  // Return Array*
    arr=getArray(arr)
    
    Declare Sub addObject Lib "Cocoa" Selector "addObject:" (receiver As Ptr, obj As CFStringRef)
    addObject(arr, SaveToolbarItemIdentifier)  // ユーザ定義アイテム
    addObject(arr, "NSToolbarSpaceItem")
    addObject(arr, "NSToolbarShowColorsItem")
    addObject(arr, "NSToolbarShowFontsItem")
    addObject(arr, "NSToolbarFlexibleSpaceItem")
    addObject(arr, "NSToolbarPrintItem")
      
    // リストを返す
    return arr
    
    注)SELの型をPtrとしていたが、CStringの方が相応しいので変更した。(未使用なので変更しなくても実害はなし。)(2018.07.30)
  10. 以下をWindow1の共有メソッド(Shared Methods)に追加
    メソッド名: toolbarItemForItemIdentifierWillBeInsertedIntoToolbar
    引数: id As Ptr, SEL As CString, toolbar As Ptr, itemIdentifier As CFStringRef, flag As Boolean
    戻り値型: Ptr
    
    // 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。
    Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
    Declare Function NSSelectorFromString Lib "Cocoa" (aSelName As CFStringRef) As Ptr
    
    // アイテムの種別が「保存」なら生成する
    if itemIdentifier = SaveToolbarItemIdentifier then
    
        // アイテムの初期化
        Dim toolbarItem1 As Ptr = NSClassFromString("NSToolbarItem")
        Declare Function alloc Lib "Cocoa" Selector "alloc" (receiver As Ptr) As Ptr
        toolbarItem1 = alloc(toolbarItem1)
        Declare Function initWithItemIdentifier Lib "Cocoa" Selector "initWithItemIdentifier:" (receiver As Ptr, identifier As CFStringRef) As Ptr
        toolbarItem1 = initWithItemIdentifier(toolbarItem1, itemIdentifier)
    
        // ラベル設定
        Declare Sub setLabel Lib "Cocoa" Selector "setLabel:" (receiver As Ptr, label As CFStringRef)
        setLabel(toolbarItem1, "Save")
        Declare Sub setPaletteLabel Lib "Cocoa" Selector "setPaletteLabel:" (receiver As Ptr, label As CFStringRef)
        setPaletteLabel(toolbarItem1, "Save")
    
        // ツールチップ設定
        Declare Sub setToolTip Lib "Cocoa" Selector "setToolTip:" (receiver As Ptr, text As CFStringRef)
        setToolTip(toolbarItem1, "Save Your Document")
    	
        // アイコン設定
        Dim bundle1 As Ptr = NSClassFromString("NSBundle")
        Declare Function mainBundle Lib "Cocoa" Selector "mainBundle" (receiver As Ptr) As Ptr
        bundle1 = mainBundle(bundle1)
        Declare Function pathForResource Lib "Cocoa" Selector "pathForResource:ofType:" (receiver As Ptr, path As CFStringRef, type As CFStringRef) As CFStringRef
        Dim filepath As String = pathForResource(bundle1,"save","png")  // プロジェクトに追加したアイコン画像ファイルの名前
        Dim image1 As Ptr = NSClassFromString("NSImage")
        image1 = alloc(image1)
        Declare Function initWithContentsOfFile Lib "Cocoa" Selector "initWithContentsOfFile:" (receiver As Ptr, path As CFStringRef) As Ptr
        image1 = initWithContentsOfFile(image1, filepath)
        Declare Sub setImage Lib "Cocoa" Selector "setImage:" (receiver As Ptr, image As Ptr)
        setImage(toolbarItem1, image1)
        
        // Target/Action設定
        Declare Sub setTarget Lib "Cocoa" Selector "setTarget:" (receiver As Ptr, actionTarget As Ptr)
        setTarget(toolbarItem1, makeTarget(itemIdentifier))  // Targetの設定
        Declare Sub setAction Lib "Cocoa" Selector "setAction:" (receiver As Ptr, actionEvent As Ptr)
        setAction(toolbarItem1, NSSelectorFromString("action:"))  // Actionの設定
    
        // 生成したアイテムを返す
        return toolbarItem1
    
    else
        return nil
    end if
    
    注)SELの型をPtrとしていたが、CStringの方が相応しいので変更した。(未使用なので変更しなくても実害はなし。)(2018.07.30)
  11. 以下をWindow1の共有プロパティ(Shared Properties)に追加
    プロパティ名: SaveToolbarItemIdentifier
    データ型: String
    標準値: My Save Toolbar Item
    
  12. 以下をWindow1の共有プロパティ(Shared Properties)に追加
    プロパティ名: TargetInstance
    データ型: Ptr
    標準値: nil
    
  13. 以下をWindow1の共有プロパティ(Shared Properties)に追加
    プロパティ名: Window1Instance
    データ型: Window1
    

 実行してみたところ、ツールバーが表示されて、アイテム及びカスタマイズが機能することを確認しました。
S Shot1

S Shot1


 おわりに

 今回は仕組みを理解するために、極力シンプルな書き方を心懸けました。
(例えば、allocした変数の一部は然るべきタイミングでReleaseするべきと思われますが、そのためにはコードを工夫する必要があります。)
 実用目的であれば、macoslibのように汎用化する(というか、そのまま利用させて頂く)ことを考えた方がいいでしょう。

 ボタン以外のセグメントやサーチフィールドは、機会を見つけてトライしてみたいと思います。の配置については、こちらのページに纏めました


 お世話になったサイト

 貴重な情報をご提供頂いている皆様に、お礼申し上げます。(以下、順不同)

 参考サイト(1):Introduction to Toolbars
 参考サイト(2):NSToolbar basics | No pain no gain
 参考サイト(3):Objective-C - Blockで動的にメソッドを実装できるクラスの作り方 - Qiita


 更新履歴

 2018.07.30 Xojoでの実装、の5,8,9,10項を改訂
 2015.10.12 共有メソッドについて、に追記と参考サイト(3)を追加。
 2015.10.08 Xojoでの実装 7項(makeTarget)のコードを修正して注を付記。
 2015.10.08 TargetInstanceを共有プロパティに追加(12項)
 2015.09.30 新規作成


[Home]  [MacSoft]  [Donation]  [History]  [Privacy Policy]  [Affiliate Policy]