ホームページ>開発ツール>Xojo / Real Studio Trial and Error・CocoaのDeclareでOS標準のタブ機能を使ってみる
Xojo / Real Studio Trial and Error
目次
CocoaのDeclareでOS標準のタブ機能を使ってみる
はじめに
以下は、Xojo Cocoaビルドについての話題です。
OS X 10.12 Sierra以降、システムレベル(Cocoa API)でウィンドウをタブ化する機能(以下、ウィンドウタブ)が加わりましたが、これをXojoからも利用できないか試してみました。
なお検証には、Xojo 2016 Release 3を用いています。(Mac mini mid 2010 + OS X 10.13.6 High Sierra)
方針
ウィンドウタブ機能は、Xojo標準でも一部は利用可能で、メニューバーにメニューを追加し、Name:View、Text:表示、とするだけで、実行時にメニュー項目として「タブバーを表示(タブバーを非表示とトグル動作)」「すべてのタブを表示」「フルスクリーンにする」が現れます。
ここで「タブバーを表示」を選択すると、タブバーが表示されます。ただし、タブの操作はできません。
一方Xcodeでは、デフォルトの設定ではXojoのケースと同様ですが、プロジェクト作成時に「Create Document-Based Application」オプションを有効にしておくと、何もしなくても動作するようになります。
そんなこともあってか、手動で設定する方法についての情報はなかなかヒットせず、それでも見つかったのが以下のサイトです。
参考サイト(1):Programmatically Add Tabs to NSWindows without NSDocument • Christian Tietze
WindowControllerベースだったり、Swiftだったりと、そのままは適用できませんが、有用であることは間違いなく、分かったことは、
さて、Xojoでどう表現するかですが、タブモードはデフォルトのまま(つまり、何もしない)で問題なさそうです。
- NSWindow.tabbingModeで、タブモードを有効に
- NSResponder.newWindowForTab(_:)で、タブ追加ボタン(+印)を表示
- NSWindow.addTabbedWindow(_:ordered:)で、ウィンドウをタブバーに追加
(注:話の流れ上、元記事と2,3の順番を入れ替えています。)
次に、タブ追加ボタン(+印)の表示ですが、NSResponderのインスタンスを生成して、ウィンドウをレスポンダーチェーンに追加します。
この時、newWindowForTab:デリゲートメソッドを実装していると、タブバーに生成ボタン(+印)が現れるようになります。
最後に、ウィンドウのタブバーへの追加ですが、newWindowForTab:メソッド内でウィンドウを生成して、addTabbedWindow:ordered:をセットします。
さらに、生成したウィンドウもレスポンダーチェーンに追加します。(追加しないと、そのウィンドウタブからは新規作成ができなくなる。)
これで、ウィンドウタブが機能するようになります。
タブの切り替え、並べ替え、クローズはシステム側がやってくれるので、そのための対策は不要です。
試行しながら分かったのは、ウィンドウタブ機能は(同じウィンドウクラスから複製した)複数のウィンドウを一箇所にまとめて表示し、タブでウィンドウごと切り替える、という点。ただし、上記サイトが示すWindowControllerベースにしてないとか、レスポンダーチェーンが実はチェーンになってない?とか、これでいいのかはよく分かりません。
なので、メニューやツールバーからの指示は、直接、各ウィンドウに届く。(つまり、どこかで振り分け処理を行う訳ではない。)
即ち、一つのウィンドウにタブパネルを置き、タブを切り替えて使う、といったものとは意味合いが異なる。
また、タブバーを表示すると、ウィンドウの高さ情報に齟齬が生じる(以前インスペクターバーを表示した時と同じ?)という問題があり、これに対応しようとすると、結構大変なことになります。(既に目処は立っているが、話を分かりやすくするため、この件は別の話題扱いとする。)
以上を踏まえ、(残りの)仕様は以下の通りとしました。
- ウィンドウの内容物は、テキストエリア1個とする。
- ウィンドウにはツールバーを置き、保存ボタンと短文追加ボタンを置く。(ツールバーアクションの、伝達経路確認用)
- メニューに、短文追加を置く。(メニューアクションの、伝達経路確認用)
- 新規タブは、常にタブ群の最後尾に追加する。(テキストエディットに倣った。)
Xojoでの実装
【ソースコードのコピー&ペーストについて】
ソースコード(グレー背景部分の全文)をコピーし、指定のウィンドウ/クラスにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
ただし、この方法は、メソッドでは問題ないようですが、イベント/アクション/プロパティでは不安定?なので、ペーストできない場合は、各項目のカッコ内を適用して下さい。
実行してみたところ、ウィンドウタブが機能することを確認しました。
- Xojoで新規プロジェクトを作成
- 以下をAppにペースト(できなければ、Sub - Endの間をOpenイベントに記述)
Sub Open() Handles Open // NSResponderのインスタンスを生成してウィンドウをレスポンダーチェーンに追加(newWindowForTab:を実装していると、タブバーに生成ボタン(+印)が現れる) NSResponder.InitResponder(Window1.Handle) // パラメーター gWinMax=gWinMax+1 Window1.Title="名称未設定 "+Str(gWinMax) // ウィンドウからタブグループを取得して保持 Declare Function tabGroup Lib "Cocoa" Selector "tabGroup" (receiver As Integer) As Ptr gTabGroup = tabGroup(Window1.Handle) End Sub
- MainMenuBar>編集メニューの次に、メニュー(Name:View、Text:表示)を追加
- MainMenuBar>表示メニューの次に、メニュー(Name:Op、Text:操作)を追加
- MainMenuBar>操作メニューに、メニュー項目(Name:OpAddmsg、Text:短文追加)を追加
- Toolbarをプロジェクトに追加(Name:Toolbar1)後、以下のアイテムを追加。その後、Window1に追加(Name:Toolbar11)。
ToolItem1(保存)/ToolItem2(スペース)/ToolItem3(短文追加)- 以下をToolbar11にペースト(できなければ、Sub - Endの間をToolbar11のActionイベントに記述)
Sub Action(item As ToolItem) Handles Action select case item.Name case "ToolItem1" SaveFile() case "ToolItem3" TextArea1.AppendText "あいうえお" end select End Sub
- Window1に、TextArea(Name:TextArea1)を追加後。Lockingプロパティで、右と下もロック。
- 以下をWindow1にペースト(できなければ、Function - End Functionの間をOpAddmsgメニューハンドラに記述)
Function OpAddmsg() As Boolean TextArea1.AppendText "あいうえお" Return True End Function
- 以下をWindow1にペースト
注)言語リファレンス>TextOutputStreamのサンプルを参考にさせて頂きました。Protected Sub SaveFile() Dim t As TextOutputStream Dim f As FolderItem f = GetSaveFolderItem(FileTypes1.Text, me.Title) If f <> Nil Then t = TextOutputStream.Create(f) t.Write(TextArea1.Text) t.Close End If End Sub
- 新規ファイルタイプ(名前は、ここではデフォルトの「FileTypes1」)を作成し、一般的なファイルタイプの追加の中から、「text/plain」を追加。
- 新規モジュール(名前は、ここでは「Globals」)を作成。
- 以下をGlobalsにペースト(できなければプロパティに、名前:gTabGroup、データ型:Ptr、を追加)
Public Property gTabGroup as Ptr
- 以下をGlobalsにペースト(できなければプロパティに、名前:gWinMax、データ型:Integer、を追加)
Public Property gWinMax as Integer
- 新規クラス(名前は、ここでは「NSResponder」)を作成。
- 以下をNSResponderにペースト
Public Shared Sub InitResponder(win As Integer) // 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。 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 ResponderInst <> nil then return end if // NSResponderを継承したカスタムクラスを作成。初回のみ // クラス名をmyNSResponder、メタクラス名をNSResponderにして、生成 Dim newClassId As Ptr = objc_allocateClassPair(NSClassFromString("NSResponder"), "myNSResponder", 0) // ランタイムに登録(参照を可能とするため) objc_registerClassPair newClassId // Delegateの対象となるメソッドを追加(newWindowForTab:をXojo側で用意したmyNewWindowForTabメソッドで受け取る。) if not class_addMethod (newClassId, NSSelectorFromString("newWindowForTab:"), AddressOf myNewWindowForTab, "i12@0:4@8") then msgBox "error1." return end if // NSResponderのインスタンス作成 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 responder As Ptr = init(alloc(newClassId)) // 引数で指定されたウィンドウをレスポンダーチェーンに追加 Declare Sub setNextResponder Lib "Cocoa" Selector "setNextResponder:" (receiver As Integer, obj As Ptr) setNextResponder(win, responder) // インスタンスを共有プロパティとして保持 ResponderInst = responder End Sub
- 以下をNSResponderにペースト
Private Shared Sub myNewWindowForTab(id as Ptr, sel as CString, sender As Ptr) // タブグループに登録されているウィンドウ(配列)を取得 Declare Function windows Lib "Cocoa" Selector "windows" (receiver As Ptr) As Ptr Dim winAry As Ptr = windows(gTabGroup) // ウィンドウ配列の最後(=最後尾のタブ)を取得 Declare Function lastObject Lib "Cocoa" Selector "lastObject" (receiver As Ptr) As Ptr Dim lastTab As Ptr = lastObject(winAry) // Window1の新規インスタンスを生成 Dim win1 As new Window1 // パラメーター gWinMax=gWinMax+1 win1.Title="名称未設定 "+Str(gWinMax) // Window1の新規インスタンスをタブとして追加 Declare Sub addTabbedWindow Lib "Cocoa" Selector "addTabbedWindow:ordered:" (receiver As Ptr, win As Integer, ord As Integer) addTabbedWindow(lastTab, win1.Handle, 1) // 1 = NSWindowAbove // Window1の新規インスタンスをレスポンダーチェーンに追加 Declare Sub setNextResponder Lib "Cocoa" Selector "setNextResponder:" (receiver As Integer, obj As Ptr) setNextResponder(win1.Handle, NSResponder.ResponderInst) End Sub
- 以下をNSResponderにペースト(できなければ共有プロパティに、名前:ResponderInst、データ型:Ptr、を追加)
Private Shared Property ResponderInst as Ptr
おわりに
繰り返しになりますが、本当にこれでいいのかはよく分かりません。更なる検証が必要かと思われます。
こちらも繰り返しになりますが、タブバーの表示/非表示によって、内容物の位置がずれる現象が発生します。何度か表示/非表示と、その都度ウィンドウリサイズを行うと、内部情報が統一されるのか、ズレがなくなっていきます。
だがこれでは不十分という場合には、既に対策の目処は立っていますので、その件は次回に続く。
お世話になったサイト
貴重な情報をご提供頂いている皆様に、お礼申し上げます。(以下、順不同)
参考サイト(1):Programmatically Add Tabs to NSWindows without NSDocument • Christian Tietze
更新履歴
2019.10.01 おわりに、に続編トピックへのリンクを追加。
2019.08.05 新規作成
[Home] [MacSoft] [Donation] [History] [Privacy Policy] [Affiliate Policy]