ホームページ開発ツール>Xojo / Real Studio Trial and Error・CocoaのDeclareでコントロールを動的に生成する

 Xojo / Real Studio Trial and Error

CocoaのDeclareでコントロールを動的に生成する

目次
 はじめに

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

 Xojoでコントロールを動的(On The Fly)に生成する方法について試行しました。

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


 方針

 コントロールのオンザフライ生成機能は、Xojo自身も持ってはいますが、それはコントロール配列であり、最初の一個目はウィンドウに配置しておかなければならないという制約がつきます。
 あらかじめ必要とされるコントロールの種類は分かっているが個数が不定、といったケースでは使えそうですが、そうでないケースでは難しそうです。

 一方Cocoaには、所謂「Interface Builderを使わない」やり方、即ち、Alloc/Initしてウィンドウのビューに貼り付ける、という手法がありますが、XojoとCocoaの親和性を考えるといけそうな気がします。
 なので、この方向で考えてみることにしました。

 さて、コントロールには様々な種類がありますが、機能という点から見ると、以下のように分類できそうです。(他にもあるかも。)
  1. ラインのように、配置したら(原則として)いじらないもの
  2. ボタンのように、アクションが発生するもの
  3. テキストエディットのように、値をセットしたりゲットしたりするもの
 2,3項については、機能部分を自分で実装する必要がありますが、例えばボタンとテキストエディットについて言えば、
  1. ボタンは、以前こちらでやったように、Objective-CのランタイムAPIを用いてTarget/Actionを設定する。
  2. テキストエディットは、Xojoネイティブでは値(テキスト)をプロパティ(.Text)として扱っていることから、それに合わせ込むこととし、計算型プロパティを使う。
 さらに、コントロールがXojoネイティブと同じ基準で扱えるよう、互換性を確保しておくことにします。現状確認されているのは、
  1. y座標系がXojoとCocoaで反転しているので、補正する。
  2. 位置/サイズが一致しないので、Xojoと同じ値を指定したら同じになるように補正する。
注)テキストエディットはテキストフィールドとテキストエリアの総称。
 あと、Objective-CのランタイムAPIですが、今まではインスタンスを1個しか必要としなかったので、クラスと同時に生成してきましたが、今回はインスタンスの個数が不定なので、分離することにします。


 Xojoでの実装

 今回は実験として、テキストフィールドと(テキストフィールドに文字列を)セット/ゲットするボタンの組を、動的に生成してみます。
  1. Xojoで新規プロジェクトを作成
  2. Window1にPopupMenuを置き、InitialValueに0〜5を記述
  3. Window1にPushButtonを置き、以下をActionイベントに記述
    Dim i, cnt As Integer
    
    // 既に生成済なら削除
    RemoveControl()
    
    // 指定された個数分の組を生成
    cnt = PopupMenu1.ListIndex
    for i=0 to cnt-1
    	
        // テキストフィールド。引数は順に、配置するウィンドウ、位置/サイズ
        Dim a As NSTextField = new NSTextField(self, NSMakeRectNSTextField(40, 20+30*i, 100, 22))
        OTFTextField.Append a  // 生成したインスタンスを保持
        
        // ボタン。引数は順に、配置するウィンドウ、位置/サイズ、外観(1 = NSRoundedBezelStyle)、キャプション、押された時に呼び出されるメソッド
        Dim b As NSButton = new NSButton(self, NSMakeRectNSButton(160, 20+30*i, 80, 20), 1, "Set "+Str(i), AddressOf ButtonClicked)
        Dim c As NSButton = new NSButton(self, NSMakeRectNSButton(250, 20+30*i, 80, 20), 1, "Get "+Str(i), AddressOf ButtonClicked)
        
    next
    
  4. 以下をWindow1のメソッドに追加
    メソッド名: ButtonClicked
    引数: sender As Ptr
    
    // senderのタグ(=ボタンの固有番号)を取得
    Declare Function tag Lib "Cocoa" selector "tag" (class_id As Ptr) As Integer
    dim no As Integer = tag(sender)
    
    // ボタンの固有番号からボタン種別を判定
    Dim cnt1, cnt2 As Integer
    cnt1 = no / 2  // 列の判定用
    cnt2 = no mod 2  // Set/Getの判定用
    
    // ボタンごとの処理
    select case cnt2
    case 0  // Set
        OTFTextField(cnt1).TextStr = "test String "+Str(cnt1)
    case 1  // Get
        msgBox OTFTextField(cnt1).TextStr
    end select
    
  5. 以下をWindow1のメソッドに追加
    メソッド名: RemoveControl
    
    Dim i, cnt As Integer
    Dim str As String
    Dim pnt3, pnt4, ary(-1) As Ptr
    
    // オブジェクトを指定してクラス名を取得する。最初に一回宣言しておけばよい。
    Declare Function NSStringFromClass Lib "Cocoa" (aClass As Ptr) As CFStringRef
    
    // ウィンドウのビューを取得
    Declare Function contentView Lib "Cocoa" selector "contentView" (class_id As Integer) As Ptr
    Dim pnt1 As Ptr = contentView(self.handle)
    
    // サブビュー配列を取得
    Declare Function subviews Lib "Cocoa" selector "subviews" (class_id As Ptr) As Ptr
    Dim pnt2 As Ptr = subviews(pnt1)
    
    // サブビューの個数を取得
    Declare Function count Lib "Cocoa" selector "count" (class_id As Ptr) As Integer
    cnt = count(pnt2)
    
    // サブビュー数だけ回す
    for i=0 to cnt-1
        
        // サブビューを順に取得
        Declare Function objectAtIndex Lib "Cocoa" selector "objectAtIndex:" (class_id As Ptr, idx As Integer) As Ptr
        pnt3 = objectAtIndex(pnt2, i)
        
        // クラス名を取得
        Declare Function myClass Lib "Cocoa" selector "class" (class_id As Ptr) As Ptr
        pnt4 = myClass(pnt3)
        
        // クラス名がオンザフライで生成したものなら、リストに追加(直接削除しないのは、個数と削除後のループ回数に齟齬が生じて、異常終了することを避けるため)
        str = NSStringFromClass(pnt4)
        if str="myTextField" or str="myCustomButton" then
            ary.Append pnt3
        end if
        
    next
      
    // リストの個数だけ回す
    for i=0 to Ubound(ary)
        
        // サブビューを削除
        Declare Sub removeFromSuperview Lib "Cocoa" selector "removeFromSuperview" (class_id As Ptr)
        removeFromSuperview(ary(i))
        
    next
    
  6. 以下をWindow1のプロパティに追加
    プロパティ名: OTFTextField(-1)
    データ型: NSTextField
    標準値: なし
    
  7. 新規クラスを作成(名前は、ここでは「NSButton」とした。)
  8. 以下をNSButtonの移譲(Delegates)に追加
    デリゲート名: ActionDelegate
    引数:sender As Ptr
    戻り値型:なし
    
  9. 以下をNSButtonのメソッド(Methods)に追加(注:Constructorは予約語で、クラスをnewした時に自動的に呼び出される。)
    メソッド名: Constructor
    引数:action As ActionDelegate
    
    // 文字列を指定してセレクタを取得する。最初に一回宣言しておけばよい。
    Declare Function NSSelectorFromString Lib "Cocoa" (aSelName As CFStringRef) As Ptr
    
    // NSButtonを継承したカスタムクラスを作成。初回のみ
    makeClass()
    
    // インスタンスを作成
    Declare Function alloc Lib "Cocoa" selector "alloc" (class_id As Ptr) As Ptr
    Declare Function initWithFrame Lib "Cocoa" selector "initWithFrame:" (obj_id As Ptr, frame As NSRect) As Ptr
    rect.y = win.Height - rect.y  // y座標系が、CocoaとXojoで反転しているので、補正
    Dim subclassId As Ptr = initWithFrame(alloc(NSButtonClass), rect)
    
    // アクションを受け取るメソッドを定義したクラスを作成。初回のみ
    makeTarget()
    
    // インスタンスを作成
    Declare Function init Lib "Cocoa" selector "init" (obj_id As Ptr) As Ptr
    Dim targetId As Ptr = init(alloc(TargetActionClass))
    
    // ターゲットアクションの設定
    Declare Sub setTarget Lib "Cocoa" Selector "setTarget:" (receiver As Ptr, actionTarget As Ptr)
    setTarget(subclassId, targetId)  // アクションを受け取るメソッドを定義
    Declare Sub setAction Lib "Cocoa" Selector "setAction:" (receiver As Ptr, actionEvent As Ptr)
    setAction(subclassId, NSSelectorFromString("action:"))
    
    // パラメータカスタマイズ
    Declare Sub setBezelStyle Lib "Cocoa" Selector "setBezelStyle:" (receiver As Ptr, tag As Integer)
    setBezelStyle(subclassId, bezel)  // ベゼルスタイル
    Declare Sub setTitle Lib "Cocoa" Selector "setTitle:" (receiver As Ptr, title As CFStringRef)
    setTitle(subclassId, title)  // キャプション
    Declare Sub setTag Lib "Cocoa" Selector "setTag:" (receiver As Ptr, tag As Integer)
    setTag(subclassId, TagCount)  // タグ(ボタンの固有番号に利用)
    TagCount = TagCount + 1
    
    // ウィンドウのビューを取得
    Declare Function contentView Lib "Cocoa" selector "contentView" (class_id As Integer) As Ptr
    Dim pnt2 As Ptr = contentView(win.handle)
    
    // 生成したコントロールをビューに追加
    Declare Sub addSubview Lib "Cocoa" selector "addSubview:" (class_id As Ptr, view As Ptr)
    addSubview(pnt2, subclassId)
    
    // Window側でActionを受け取るメソッドを登録
    ActionHandler = action
    
  10. 以下をNSButtonの共有メソッド(Shared Methods)に追加
    メソッド名: ActionEvent
    引数:id As Ptr, SEL As CString, sender As Ptr
    
    // Constructorで登録した、Actionを受け取るメソッドを呼び出す
    ActionHandler.Invoke(sender)
    
    注)SELの型をPtrとしていたが、CStringの方が相応しいので変更した。(未使用なので変更しなくても実害はなし。)(2018.07.30)
  11. 以下をNSButtonの共有メソッド(Shared Methods)に追加
    メソッド名: makeClass
    
    // 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。
    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 NSButtonClass <> nil then
        return
    end if
    
    // クラス名をmyCustomButton、メタクラス名をNSButtonにして、生成
    Dim newClassId As Ptr = objc_allocateClassPair(NSClassFromString("NSButton"), "myCustomButton", 0)
    // ランタイムに登録(参照を可能とするため)
    objc_registerClassPair newClassId
    // Delegateはないので何もしない
    
    // クラスを保持
    NSButtonClass = newClassId
    
  12. 以下をNSButtonの共有メソッド(Shared Methods)に追加
    メソッド名: makeTarget
    
    // 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。
    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 TargetActionClass <> nil then
        return
    end if
    
    // クラス名をmyCustomButtonAction、メタクラス名をNSObjectにして、生成
    Dim newClassId As Ptr = objc_allocateClassPair(NSClassFromString("NSObject"), "myCustomButtonAction", 0)
    // ランタイムに登録(参照を可能とするため)
    objc_registerClassPair newClassId
    // Tarrgetに送られてきたActionの受け口となるメソッドを追加(action:をXojo側で用意したactionEventメソッドで受け取る。)
    if not class_addMethod (newClassId, NSSelectorFromString("action:"), AddressOf actionEvent, "@@:@") then
        msgBox "error."
        return
    end if
    
    // クラスを保持
    TargetActionClass = newClassId
    
  13. 以下をNSButtonの共有プロパティ(Shared Properties)に追加(注:データ型は、デリゲート名と一致させる。)
    プロパティ名: ActionHandler
    データ型: ActionDelegate
    
  14. 以下をNSButtonの共有プロパティ(Shared Properties)に追加
    プロパティ名: NSButtonClass
    データ型: Ptr
    
  15. 以下をNSButtonの共有プロパティ(Shared Properties)に追加
    プロパティ名: TagCount
    データ型: Integer
    標準値: 0
    
  16. 以下をNSButtonの共有プロパティ(Shared Properties)に追加
    プロパティ名: TargetActionClass
    データ型: Ptr
    
  17. 新規クラスを作成(名前は、ここでは「NSTextField」とした。)
  18. 以下をNSTextFieldのメソッド(Methods)に追加(注:Constructorは予約語で、クラスをnewした時に自動的に呼び出される。)
    メソッド名: Constructor
    引数:win As Window, rect As NSRect
    
    // NSTextFieldを継承したカスタムクラスを作成。初回のみ
    makeClass()
    
    // インスタンスを作成
    Declare Function alloc Lib "Cocoa" selector "alloc" (class_id As Ptr) As Ptr
    Declare Function initWithFrame Lib "Cocoa" selector "initWithFrame:" (obj_id As Ptr, frame As NSRect) As Ptr
    rect.y = win.Height - rect.y  // y座標系が、CocoaとXojoで反転しているので、補正
    Dim subclassId As Ptr = initWithFrame(alloc(NSTextfieldClass), rect)
    
    // ウィンドウのビューを取得
    Declare Function contentView Lib "Cocoa" selector "contentView" (class_id As Integer) As Ptr
    Dim pnt2 As Ptr = contentView(win.handle)
    
    // 生成したコントロールをビューに追加
    Declare Sub addSubview Lib "Cocoa" selector "addSubview:" (class_id As Ptr, view As Ptr)
    addSubview(pnt2, subclassId)
    
    // インスタンスを保持
    InstancePtr = subclassId
    
  19. 以下をNSTextFieldのプロパティに追加
    プロパティ名: InstancePtr
    データ型: Ptr
    
  20. 以下をNSTextFieldの計算型プロパティに追加(注:計算型プロパティも、Properties内に生成される。)
    プロパティ名: TextStr
    データ型: String
    
  21. 以下をTextStrのGetメソッドに追加
    // テキストの取得
    Declare Function stringValue Lib "Cocoa" selector "stringValue" (class_id As Ptr) As CFStringRef
    Dim str As String = stringValue(InstancePtr)
    
    // テキストを返す
    return str
    
  22. 以下をTextStrのSetメソッドに追加
    // テキストをセット
    Declare Sub setStringValue Lib "Cocoa" selector "setStringValue:" (class_id As Ptr, str As CFStringRef)
    setStringValue(InstancePtr, value)
    
  23. 以下をNSTextFieldの共有メソッド(Shared Methods)に追加
    メソッド名: makeClass
    
    // 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。
    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 NSTextfieldClass <> nil then
        return
    end if
    
    // クラス名をmyTextField、メタクラス名をNSTextFieldにして、生成
    Dim newClassId As Ptr = objc_allocateClassPair(NSClassFromString("NSTextField"), "myTextField", 0)
    // ランタイムに登録(参照を可能とするため)
    objc_registerClassPair newClassId
    // Delegateはないので何もしない
    
    // クラスを保持
    NSTextfieldClass = newClassId
    
  24. 以下をNSTextFieldの共有プロパティ(Shared Properties)に追加
    プロパティ名: NSTextfieldClass
    データ型: Ptr
    
  25. 新規モジュールを作成(名前は、ここでは「Globals」とした。)
  26. 以下をGlobalsのメソッド(Methods)に追加(注:macoslibのNSMakeRectをベースにさせて頂きました。)
    メソッド名: NSMakeRectNSButton
    引数:x as Double, y as Double, w as Double, h as Double
    戻り値:NSRect
    
    // XojoとCocoaで位置/サイズが一致しないことの補正を含む。ただし、個別の現物合わせ値なので更なる調整が必要かも。
    dim r as NSRect
    r.x = x - 6
    r.y = y + 24
    r.w = w + 12
    r.h = h + 4
    return r
    
  27. 以下をGlobalsのメソッド(Methods)に追加(注:macoslibのNSMakeRectをベースにさせて頂きました。)
    メソッド名: NSMakeRectNSTextField
    引数:x as Double, y as Double, w as Double, h as Double
    戻り値:NSRect
    
    // XojoとCocoaで位置/サイズが一致しないことの補正を含む。ただし、個別の現物合わせ値なので更なる調整が必要かも。
    dim r as NSRect
    r.x = x
    r.y = y + h
    r.w = w
    r.h = h
    return r
    
  28. NSRectをGlobalsの構造体(Structures)にコピー(注:macoslibからコピーさせて頂きました。)
 実行してみたところ、コントロールが生成され、ボタンが機能することを確認しました。
S Shot1


 おわりに

 今回のサンプルは、方針のところで述べた「あらかじめ必要とされるコントロールの種類は分かっているが個数が不定」に該当するものと言えなくもなく、そうであれば、Xojoネイティブの機能でも事足りてしまいます。
 また、今回は実装項目を最小限に抑えられましたが、もっと複雑なコントロール(例えばListBox)では実装すべき項目も多く、更に、allocした変数のReleaseといった点への配慮等も必要になることから、相当な手間を覚悟しなければならなさそうです。
 これらを考えると、実用性という点ではさほどメリットはないかもしれません。

 なお現状、ウィンドウをリサイズ可能とした場合、リサイズするとコントロールがウィンドウ下辺に追随して動いてしまう、という点が確認されています。
 これは、(前述の通り)Cocoaの座標系ではy軸の原点がウィンドウ下辺にあるためと思われますが、リサイズする場合は別途配慮が必要となります。

 参考サイト(1):(旧) Cocoaの日々: 内容によってウィンドウのサイズを変える


 お世話になったサイト

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

 参考サイト(1):(旧) Cocoaの日々: 内容によってウィンドウのサイズを変える


 更新履歴

 2018.07.30 Xojoでの実装の10項を改訂
 2016.07.28 新規作成


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