ホームページ開発ツール>Xojo / Real Studio Trial and Error・附録・通知センター機能をクラス化する

 Xojo / Real Studio Trial and Error

附録・通知センター機能をクラス化する

目次
 はじめに

 通知センター機能をクラス化してみました。

 なお検証には、Xojo 2015 Release 4.1を用いています。(Mac mini mid 2010 + OS X 10.11.4 El Capitan)


 方針

 クラス化にあたっては、以下の点が課題となりました。
  1. Delegateメソッドは共有メソッドだが、パラメータの設定はインスタンス側で行うので、どう受け渡すか?
  2. 1項に関連して、インスタンスを複数個置いた場合、どのインスタンスに処理を投げるかの判断が必要になる。
  3. パラメータの設定をメソッドで行うとすると「このクラスを使うときはこれこれのメソッドを必ず実装してください」となってしまい、使い勝手が悪い。
  4. 初期化を通常のメソッドで行おうとすると、3項と同様の問題が発生する。
 1項については、クラス内にインスタンスを保持するプロパティを用意して、ここからインスタンス側のメソッドをコールすることとしました。
 2項については、プロパティを配列化し、NSUserNotificationのUserInfoに添字を格納することで、通知を受け取った時に、どのインスタンスに渡せばいいかを判定できるようにしました。

 3項については、メソッドでなく、イベントを使用することとしました。イベントには以下のメリットがあります
 (1) パラメータを指定しなくていい場合は、何もしなくていい。(イベントはパスされ、戻り値はデフォルトの値(今回のケースではfalse)が適用される。)
 (2) パラメータを指定する場合も、「イベントハンドラ...」メニューから選んで設定できるので、名前や引数を正確に覚えておかなくていい。

 4項については、Constructorを使うこととしました。Constructorはインスタンス生成時に自動的に呼ばれるため、ユーザが呼び出す必要がなくなります。


 Xojoでの実装
  1. Xojoで新規プロジェクトを作成
  2. 新規クラスを作成(名前は、ここでは「NotificationCenter」とした。)
  3. 以下をNotificationCenterのイベント定義(Event Definitions)に追加
    イベント名: ShouldPresentNotification
    引数: id As Ptr, SEL As CString, center As Ptr, notification As Ptr
    戻り値型: Boolean
    
    注)SELの型をPtrとしていたが、CStringの方が相応しいので変更した。(未使用なので変更しなくても実害はなし。)(2018.07.30)
  4. 以下をNotificationCenterのメソッドに追加
    メソッド名: Constructor
    引数: なし
    
    // 未設定なら
    if not DelegateFlg then
        
        // 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。
        Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
        Declare Function NSSelectorFromString Lib "Cocoa" (aSelName As CFStringRef) As Ptr
        
        // Delegateを設定するための仕込み
        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
        
        Dim newClassId As Ptr = objc_allocateClassPair(NSClassFromString("NSObject"), "myNSUserNotificationCenterDelegate", 0)
        if not class_addMethod (newClassId, NSSelectorFromString("userNotificationCenter:shouldPresentNotification:"), AddressOf ySharedShouldPresentNotification, "B@:@@") then
            msgBox "error."
            return
        end if
        objc_registerClassPair newClassId
        
        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))
        
        // defaultUserNotificationCenterを取得して保持
        NCenter = NSClassFromString("NSUserNotificationCenter")
        Declare Function defaultUserNotificationCenter Lib "Cocoa" Selector "defaultUserNotificationCenter" (receiver As Ptr) As Ptr
        NCenter = defaultUserNotificationCenter(NCenter)
        
        // Delegateの設定
        Declare Sub setDelegate Lib "Cocoa" selector "setDelegate:" (receiver As Ptr, id As Ptr)
        setDelegate NCenter, delegateId
        
        // 設定済フラグ・オン
        DelegateFlg = true
        
    end if
    
  5. 以下をNotificationCenterのメソッドに追加
    メソッド名: NotifyAfterSeconds
    引数: title As String, info As String, ival As Integer
    戻り値型: なし
    
    // インスタンスを保持(イベントのレイズはインスタンスメソッドからしかできないが、インスタンスメソッドはクラスから直接呼び出せないため、確保しておく)
    Inst.Append self
    
    // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
    Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
    
    // UserNotificationの初期化
    Dim notification As Ptr = NSClassFromString("NSUserNotification")
    Declare Function notificationAlloc Lib "Cocoa" Selector "alloc" (receiver As Ptr) As Ptr
    notification = notificationAlloc(notification)
    Declare Function notificationInit Lib "Cocoa" Selector "init" (receiver As Ptr) As Ptr
    notification = notificationInit(notification)
    
    // Titleのセット
    Declare Sub setTitle Lib "Cocoa" Selector "setTitle:" (receiver As Ptr, id As CFStringRef)
    setTitle(notification, title)
    
    // InformativeTextのセット
    Declare Sub setInformativeText Lib "Cocoa" Selector "setInformativeText:" (receiver As Ptr, id As CFStringRef)
    setInformativeText(notification, info)
    
    // DeliveryDateのセット(ここでは指定秒数後の時刻をセットしている)
    Dim date1 As Ptr = NSClassFromString("NSDate")
    Declare Function date Lib "Cocoa" Selector "date" (receiver As Ptr) As Ptr
    date1 = date(date1)
    Dim date2 As Ptr = NSClassFromString("NSDate")
    Declare Function dateWithTimeInterval Lib "Cocoa" Selector "dateWithTimeInterval:sinceDate:" (receiver As Ptr, id1 As Double, id2 As Ptr) As Ptr
    date2 = dateWithTimeInterval(date2, ival, date1)
    Declare Sub setDeliveryDate Lib "Cocoa" Selector "setDeliveryDate:" (receiver As Ptr, id As Ptr)
    setDeliveryDate(notification, date2)
    
    // SoundNameのセット
    Declare Sub setSoundName Lib "Cocoa" Selector "setSoundName:" (receiver As Ptr, id As CFStringRef)
    setSoundName(notification, "DefaultSoundName")
    
    // UserInfoのセット(インスタンスを格納した配列の添字を保持するために使用)
    Dim dic As Ptr = NSClassFromString("NSMutableDictionary")  // クラスメソッドなので、まずNSMutableDictionaryクラスを取得
    Declare Function getDictionary Lib "Cocoa" Selector "dictionary" (receiver As Ptr) As Ptr  // Return NSMutableDictionary*
    dic = getDictionary(dic)
    Declare Sub addObjectForKey Lib "Cocoa" Selector "setObject:forKey:" (receiver As Ptr, obj As CFStringRef, key As CFStringRef)
    addObjectForKey(dic, Str(Ubound(Inst)), "instNo")  // 配列の添字をセット(冒頭で配列にAppendしているので、最後が当該インスタンス)
    Declare Sub setUserInfo Lib "Cocoa" Selector "setUserInfo:" (receiver As Ptr, id As Ptr)
    setUserInfo(notification, dic)
    
    // 通知の発行
    Declare Sub scheduleNotification Lib "Cocoa" Selector "scheduleNotification:" (receiver As Ptr, id As Ptr)
    scheduleNotification(NCenter, notification)
    
  6. 以下をNotificationCenterのメソッドに追加
    メソッド名: yShouldPresentNotification
    引数: id As Ptr, SEL As CString, center As Ptr, notification As Ptr
    戻り値型: Boolean
    
    // イベントをレイズ
    return RaiseEvent ShouldPresentNotification(id, sel, center, notification)
    
    注)SELの型をPtrとしていたが、CStringの方が相応しいので変更した。(未使用なので変更しなくても実害はなし。)(2018.07.30)
  7. 以下をNotificationCenterの共有メソッド(Shared Methods)に追加
    メソッド名: ySharedShouldPresentNotification
    引数: id As Ptr, SEL As CString, center As Ptr, notification As Ptr
    戻り値型: Boolean
    
    // notificationのuserInfoからインスタンスを格納した配列の添字を取得
    Declare Function userInfo Lib "Cocoa" Selector "userInfo" (receiver As Ptr) As Ptr
    Dim dic As Ptr = userInfo(notification)
    Declare Function objectForKey Lib "Cocoa" Selector "objectForKey:" (receiver As Ptr, key As CFStringRef) As CFStringRef
    Dim noStr As String = objectForKey(dic, "instNo")
    
    // イベントをレイズするため、通知を発行したインスタンスのメソッドを起動
    return Inst(Val(noStr)).yShouldPresentNotification(id, sel, center, notification)
    
    注)SELの型をPtrとしていたが、CStringの方が相応しいので変更した。(未使用なので変更しなくても実害はなし。)(2018.07.30)
  8. 以下をNotificationCenterの共有プロパティ(Shared Properties)に追加
    名前: DelegateFlg
    データ型: Boolean
    標準値: なし
    
  9. 以下をNotificationCenterの共有プロパティ(Shared Properties)に追加
    名前: Inst(-1)
    データ型: NotificationCenter
    標準値: なし
    
  10. 以下をNotificationCenterの共有プロパティ(Shared Properties)に追加
    名前: NCenter
    データ型: Ptr
    標準値: なし
    
  11. Xojoプロジェクトウィンドウ>ナビゲーターのWindow1をクリックし、NotificationCenterをレイアウトエディタにドラッグ&ドロップ(2回)
    (レイアウトエディタ下部とナビゲーターのWindow1>Controlsに、NotificationCenterクラスのインスタンス(NotificationCenter1と2)が生成される)
    S Shot1

  12. 以下をNotificationCenter1と2のShouldPresentNotificationイベントに記述
    return true  // trueを返すことで、自身が最前面にあってもバナー表示が可能に
    
  13. Window1にPushButtonを置き、以下をActionイベントに記述
    // NotificationCenter1に対し、タイトルを"Hello World 1"、インフォメーションを"my message 1"、インターバルタイムを3秒後に設定
    NotificationCenter1.NotifyAfterSeconds("Hello World 1","my message 1",3)
    
    // NotificationCenter2に対し、タイトルを"Hello World 2"、インフォメーションを"my message 2"、インターバルタイムを10秒後に設定
    NotificationCenter2.NotifyAfterSeconds("Hello World 2","my message 2",10)
    
 実行してみたところ、通知が表示されることを確認しました。


 おわりに

 Delegateのための、Objective-CのランタイムAPIを用いた動的クラス生成は、NSUserNotificationCenterに対して行うものなので、本来一回行えばいいものです。
 ですが、現状Constructor内に置いているために、インスタンス生成時に繰り返し呼ばれることになり、それをフラグで抑止するという、あまり筋がいいとは言えない実装になっています。
 かといって、他に(自動的に起動される)適当なイベントやメソッドが思いつかないこともあり、継続調査となりそうです。
macoslibは、class_addMethodの戻り値がfalseの場合は(既に作成済と判断して?)何もしない、っぽい?(ざっと眺めただけなので、不正確かも…)
 あと、UserInfoに配列の添字ではなく、インスタンスまたはメソッドそれ自身を登録できればよりシンプルになるのですが、方法がわかりません。これも今後の課題となりそうです。


 お世話になったサイト

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


 更新履歴

 2018.07.30 Xojoでの実装、の3,6,7項を改訂
 2016.05.14 新規作成


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