ホームページ開発ツール>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:表示、とするだけで、実行時にメニュー項目として「タブバーを表示(タブバーを非表示とトグル動作)」「すべてのタブを表示」「フルスクリーンにする」が現れます。
 ここで「タブバーを表示」を選択すると、タブバーが表示されます。ただし、タブの操作はできません。
S Shot1
 一方Xcodeでは、デフォルトの設定ではXojoのケースと同様ですが、プロジェクト作成時に「Create Document-Based Application」オプションを有効にしておくと、何もしなくても動作するようになります。
 そんなこともあってか、手動で設定する方法についての情報はなかなかヒットせず、それでも見つかったのが以下のサイトです。

 参考サイト(1):Programmatically Add Tabs to NSWindows without NSDocument • Christian Tietze

 WindowControllerベースだったり、Swiftだったりと、そのままは適用できませんが、有用であることは間違いなく、分かったことは、
  1. NSWindow.tabbingModeで、タブモードを有効に
  2. NSResponder.newWindowForTab(_:)で、タブ追加ボタン(+印)を表示
  3. NSWindow.addTabbedWindow(_:ordered:)で、ウィンドウをタブバーに追加
    (注:話の流れ上、元記事と2,3の順番を入れ替えています。)
 さて、Xojoでどう表現するかですが、タブモードはデフォルトのまま(つまり、何もしない)で問題なさそうです。
 次に、タブ追加ボタン(+印)の表示ですが、NSResponderのインスタンスを生成して、ウィンドウをレスポンダーチェーンに追加します。
 この時、newWindowForTab:デリゲートメソッドを実装していると、タブバーに生成ボタン(+印)が現れるようになります。

 最後に、ウィンドウのタブバーへの追加ですが、newWindowForTab:メソッド内でウィンドウを生成して、addTabbedWindow:ordered:をセットします。
 さらに、生成したウィンドウもレスポンダーチェーンに追加します。(追加しないと、そのウィンドウタブからは新規作成ができなくなる。)

 これで、ウィンドウタブが機能するようになります。
 タブの切り替え、並べ替え、クローズはシステム側がやってくれるので、そのための対策は不要です。
試行しながら分かったのは、ウィンドウタブ機能は(同じウィンドウクラスから複製した)複数のウィンドウを一箇所にまとめて表示し、タブでウィンドウごと切り替える、という点。
なので、メニューやツールバーからの指示は、直接、各ウィンドウに届く。(つまり、どこかで振り分け処理を行う訳ではない。)
即ち、一つのウィンドウにタブパネルを置き、タブを切り替えて使う、といったものとは意味合いが異なる。
 ただし、上記サイトが示すWindowControllerベースにしてないとか、レスポンダーチェーンが実はチェーンになってない?とか、これでいいのかはよく分かりません。
 また、タブバーを表示すると、ウィンドウの高さ情報に齟齬が生じる(以前インスペクターバーを表示した時と同じ?)という問題があり、これに対応しようとすると、結構大変なことになります。(既に目処は立っているが、話を分かりやすくするため、この件は別の話題扱いとする。)

 以上を踏まえ、(残りの)仕様は以下の通りとしました。

 Xojoでの実装
【ソースコードのコピー&ペーストについて】
ソースコード(グレー背景部分の全文)をコピーし、指定のウィンドウ/クラスにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
ただし、この方法は、メソッドでは問題ないようですが、イベント/アクション/プロパティでは不安定?なので、ペーストできない場合は、各項目のカッコ内を適用して下さい。
  1. Xojoで新規プロジェクトを作成
  2. 以下を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
    
  3. MainMenuBar>編集メニューの次に、メニュー(Name:View、Text:表示)を追加
  4. MainMenuBar>表示メニューの次に、メニュー(Name:Op、Text:操作)を追加
  5. MainMenuBar>操作メニューに、メニュー項目(Name:OpAddmsg、Text:短文追加)を追加
  6. Toolbarをプロジェクトに追加(Name:Toolbar1)後、以下のアイテムを追加。その後、Window1に追加(Name:Toolbar11)。
    ToolItem1(保存)/ToolItem2(スペース)/ToolItem3(短文追加)
  7. 以下を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
    
  8. Window1に、TextArea(Name:TextArea1)を追加後。Lockingプロパティで、右と下もロック。
  9. 以下をWindow1にペースト(できなければ、Function - End Functionの間をOpAddmsgメニューハンドラに記述)
    Function OpAddmsg() As Boolean
      TextArea1.AppendText "あいうえお"
      
      Return True
    End Function
    
  10. 以下をWindow1にペースト
    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
    
    注)言語リファレンス>TextOutputStreamのサンプルを参考にさせて頂きました。
  11. 新規ファイルタイプ(名前は、ここではデフォルトの「FileTypes1」)を作成し、一般的なファイルタイプの追加の中から、「text/plain」を追加。
  12. 新規モジュール(名前は、ここでは「Globals」)を作成。
  13. 以下をGlobalsにペースト(できなければプロパティに、名前:gTabGroup、データ型:Ptr、を追加)
    Public Property gTabGroup as Ptr
    
  14. 以下をGlobalsにペースト(できなければプロパティに、名前:gWinMax、データ型:Integer、を追加)
    Public Property gWinMax as Integer
    
  15. 新規クラス(名前は、ここでは「NSResponder」)を作成。
  16. 以下を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
    
  17. 以下を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
    
  18. 以下をNSResponderにペースト(できなければ共有プロパティに、名前:ResponderInst、データ型:Ptr、を追加)
    Private Shared Property ResponderInst as Ptr
    
 実行してみたところ、ウィンドウタブが機能することを確認しました。
S Shot2


 おわりに

 繰り返しになりますが、本当にこれでいいのかはよく分かりません。更なる検証が必要かと思われます。

 こちらも繰り返しになりますが、タブバーの表示/非表示によって、内容物の位置がずれる現象が発生します。何度か表示/非表示と、その都度ウィンドウリサイズを行うと、内部情報が統一されるのか、ズレがなくなっていきます。
 だがこれでは不十分という場合には、既に対策の目処は立っていますので、その件は次回に続く


 お世話になったサイト

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

 参考サイト(1):Programmatically Add Tabs to NSWindows without NSDocument • Christian Tietze


 更新履歴

 2019.10.01 おわりに、に続編トピックへのリンクを追加。
 2019.08.05 新規作成


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