ホームページ>開発ツール>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の親和性を考えるといけそうな気がします。
なので、この方向で考えてみることにしました。
さて、コントロールには様々な種類がありますが、機能という点から見ると、以下のように分類できそうです。(他にもあるかも。)
2,3項については、機能部分を自分で実装する必要がありますが、例えばボタンとテキストエディットについて言えば、
- ラインのように、配置したら(原則として)いじらないもの
- ボタンのように、アクションが発生するもの
- テキストエディットのように、値をセットしたりゲットしたりするもの
さらに、コントロールがXojoネイティブと同じ基準で扱えるよう、互換性を確保しておくことにします。現状確認されているのは、
- ボタンは、以前こちらでやったように、Objective-CのランタイムAPIを用いてTarget/Actionを設定する。
- テキストエディットは、Xojoネイティブでは値(テキスト)をプロパティ(.Text)として扱っていることから、それに合わせ込むこととし、計算型プロパティを使う。
- y座標系がXojoとCocoaで反転しているので、補正する。
- 位置/サイズが一致しないので、Xojoと同じ値を指定したら同じになるように補正する。
注)テキストエディットはテキストフィールドとテキストエリアの総称。あと、Objective-CのランタイムAPIですが、今まではインスタンスを1個しか必要としなかったので、クラスと同時に生成してきましたが、今回はインスタンスの個数が不定なので、分離することにします。
Xojoでの実装
今回は実験として、テキストフィールドと(テキストフィールドに文字列を)セット/ゲットするボタンの組を、動的に生成してみます。
実行してみたところ、コントロールが生成され、ボタンが機能することを確認しました。
- Xojoで新規プロジェクトを作成
- Window1にPopupMenuを置き、InitialValueに0〜5を記述
- 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
- 以下を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
- 以下を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
- 以下をWindow1のプロパティに追加
プロパティ名: OTFTextField(-1) データ型: NSTextField 標準値: なし
- 新規クラスを作成(名前は、ここでは「NSButton」とした。)
- 以下をNSButtonの移譲(Delegates)に追加
デリゲート名: ActionDelegate 引数:sender As Ptr 戻り値型:なし
- 以下を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
- 以下をNSButtonの共有メソッド(Shared Methods)に追加
注)SELの型をPtrとしていたが、CStringの方が相応しいので変更した。(未使用なので変更しなくても実害はなし。)(2018.07.30)メソッド名: ActionEvent 引数:id As Ptr, SEL As CString, sender As Ptr // Constructorで登録した、Actionを受け取るメソッドを呼び出す ActionHandler.Invoke(sender)
- 以下を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
- 以下を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
- 以下をNSButtonの共有プロパティ(Shared Properties)に追加(注:データ型は、デリゲート名と一致させる。)
プロパティ名: ActionHandler データ型: ActionDelegate
- 以下をNSButtonの共有プロパティ(Shared Properties)に追加
プロパティ名: NSButtonClass データ型: Ptr
- 以下をNSButtonの共有プロパティ(Shared Properties)に追加
プロパティ名: TagCount データ型: Integer 標準値: 0
- 以下をNSButtonの共有プロパティ(Shared Properties)に追加
プロパティ名: TargetActionClass データ型: Ptr
- 新規クラスを作成(名前は、ここでは「NSTextField」とした。)
- 以下を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
- 以下をNSTextFieldのプロパティに追加
プロパティ名: InstancePtr データ型: Ptr
- 以下をNSTextFieldの計算型プロパティに追加(注:計算型プロパティも、Properties内に生成される。)
プロパティ名: TextStr データ型: String
- 以下をTextStrのGetメソッドに追加
// テキストの取得 Declare Function stringValue Lib "Cocoa" selector "stringValue" (class_id As Ptr) As CFStringRef Dim str As String = stringValue(InstancePtr) // テキストを返す return str
- 以下をTextStrのSetメソッドに追加
// テキストをセット Declare Sub setStringValue Lib "Cocoa" selector "setStringValue:" (class_id As Ptr, str As CFStringRef) setStringValue(InstancePtr, value)
- 以下を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
- 以下をNSTextFieldの共有プロパティ(Shared Properties)に追加
プロパティ名: NSTextfieldClass データ型: Ptr
- 新規モジュールを作成(名前は、ここでは「Globals」とした。)
- 以下を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
- 以下を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
- NSRectをGlobalsの構造体(Structures)にコピー(注:macoslibからコピーさせて頂きました。)
おわりに
今回のサンプルは、方針のところで述べた「あらかじめ必要とされるコントロールの種類は分かっているが個数が不定」に該当するものと言えなくもなく、そうであれば、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]