ホームページ開発ツール>Xojo / Real Studio Trial and Error・CocoaのDeclareでOS標準のタブ機能を使ってみる・高さ調整付

 Xojo / Real Studio Trial and Error

CocoaのDeclareでOS標準のタブ機能を使ってみる・高さ調整付

目次
 はじめに

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

 タブバーの表示/非表示によって、内容物の表示に不具合が発生する件の対策を考えてみました。
 なお検証には、Xojo 2016 Release 3を用いています。(Mac mini mid 2010 + OS X 10.13.6 High Sierra)


 方針

 前回記述した通り、タブバーを表示すると、内容物の表示に不具合が発生することが確認されています。
 もう少し具体的に言うと、テキストエリアを上下ともロックしている場合、タブバーが非表示では問題ありませんが、表示では、下にずれたように見えてしまいます。
S Shot1S Shot2
 何度か表示/非表示と、その都度ウィンドウリサイズを行うとズレがなくなっていくことから、タブバーのようなXojo標準でないパーツを追加したことで、Xojo内部での情報の不統一が発生しているのではないかという気がします(あくまで推測ですが)。

 さて、どう対処するかですが、仕様については、テキストエディットの挙動を参考にすると、ウィンドウサイズは不変で、テキストエリアサイズをウィンドウにフィットさせる、というのが妥当そうです。
 その線に沿って色々試してはみたのですが、(途中から合ってくるその)タイミングを掴め切れないことから、結局、自分で設定することにしました。

 設定する値はテキストエリアの高さですが、取得する値はウィンドウの高さになります。
 ただし、Xojo側からですと、(辻褄が合っている、といえばそうなのですが)タブバーの有無で値が変わってしまって、取得元としては適切とは言えません。
 一方、Cocoa側からの取得だと不変なので、こちらを使うことにします。

 設定のタイミングとして考えられるのは、以下の三つです。
 1. タブバーを表示(非表示)メニューの選択時
 2. ウィンドウのリサイズ時
 3. 起動時

 メニューについては、Xojo標準であればメニューハンドラー内で処理すればいいのですが、今回のようにシステム側が付加したものはそうはいかないため、代わりにNSNotificationCenterでメニュー操作時の通知をキャッチして、目的のメニュー項目だったら処理を行うようにします。
 一方、ウィンドウのリサイズは、XojoのResizingとResizedイベントで処理します。

 最後に起動時ですが、前回終了時の状態がファイルに保存されていて、それが復元されるので、合わせて設定を変えるようにします。
 設定ファイルの名前は「Bundle Identifire + .plist」になります。(今回の例では"com.mycompany.tabbarmenu.plist")
 NSWindowTabbingShoudShowTabBarKey-XOJWindow-XOJWindowController-(null)-VT-FSがキーで、これがあれば表示、なければ非表示、ということのようです。(注:実験的に確認したもので、正式な仕様は未調査。)

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

 Xojoでの実装
【ソースコードのコピー&ペーストについて】
ソースコード(グレー背景部分の全文)をコピーし、指定のウィンドウ/クラスにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
ただし、この方法は、メソッドでは問題ないようですが、イベント/アクション/プロパティでは不安定?なので、ペーストできない場合は、各項目のカッコ内を適用して下さい。
  1. 前回プロジェクトをベースとする
  2. AppのOpenイベントを、以下に差し替え
    Sub Open() Handles Open
      // Window1の現在の高さ(リサイズすると書き換えられる)
      gWin1Height=gGetWinHeight(Window1.Handle)//Window1.Height
      
      // 通知センターに通知名(メニューアイテムが選択された)と受け取り手を登録
      NSNotificationCenter.InitCenter("NSMenuDidSendActionNotification")
      
      // NSResponderのインスタンスを生成してウィンドウをレスポンダーチェーンに追加(newWindowForTabを実装していると、タブバーに生成ボタン(+印)が現れる)
      NSResponder.InitResponder(Window1.Handle)
      
      // パラメーター
      gWinMax=gWinMax+1
      Window1.Title="名称未設定 "+Str(gWinMax)
      
      // 起動時のタブバー表示状態の取得
      gTabShow=GetTabbarStatus()
      
      // タブバーが表示状態ならTextArea高さを調整
      if gTabShow then
        Window1.TextArea1.Height=gWin1Height-kTabbarHeight-(kTopMargin+kBottomMargin)
      end if
      
      // ウィンドウからタブグループを取得して保持
      Declare Function tabGroup Lib "Cocoa" Selector "tabGroup" (receiver As Integer) As Ptr
      gTabGroup = tabGroup(Window1.Handle)
    End Sub
    
  3. 以下をAppにペースト
    Private Function GetTabbarStatus() as Boolean
      // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
      Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
      
      // plistファイルからdataを取得
      Dim path As String = SpecialFolder.Preferences.Child("com.mycompany.tabbarmenu.plist").NativePath  // ファイルパスの取得
      Dim data As Ptr = NSClassFromString("NSData")  // クラスメソッドなので、まずNSDataクラスを取得
      Declare Function dataWithContentsOfFile Lib "Cocoa" Selector "dataWithContentsOfFile:" (receiver As Ptr, path As CFStringRef) As ptr
      data = dataWithContentsOfFile(data, path)
      
      // plist形式に変換
      Dim option As Integer = 0
      #if Target32Bit
        Dim format As New MemoryBlock(4)
        Dim err As New MemoryBlock(4)
      #endif
      #if Target64Bit
        Dim format As New MemoryBlock(8)
        Dim err As New MemoryBlock(8)
      #endif
      Dim pls As Ptr = NSClassFromString("NSPropertyListSerialization")  // クラスメソッドなので、まずNSPropertyListSerializationクラスを取得
      Declare Function propertyListWithData Lib "Cocoa" Selector "propertyListWithData:options:format:error:" (receiver As Ptr, data As Ptr, option As Integer, format As Ptr, err As Ptr) As Ptr
      Dim plist As Ptr = propertyListWithData(pls, data, option, format, err)  // 
      #if Target32Bit
        if err.UInt32Value(0) <> 0 then  // エラー時
          return false
        end if
      #endif
      #if Target64Bit
        if err.UInt64Value(0) <> 0 then  // エラー時
          return false
        end if
      #endif
      
      // 全てのキーを取得して、NSWindowTabbingShoudShowTabBarKey-XOJWindow-XOJWindowController-(null)-VT-FSが含まれているかチェック
      Dim flg As Boolean = false
      Declare Function count Lib "Cocoa" Selector "count" (receiver As Ptr) As Integer
      Dim cnt As Integer = count(plist)
      Declare Function allKeys Lib "Cocoa" Selector "allKeys" (receiver As Ptr) As Ptr
      Dim keyAry As Ptr = allKeys(plist)
      for i As Integer =0 to cnt-1
        Declare Function objectAtIndexS Lib "Cocoa" Selector "objectAtIndex:" (receiver As Ptr, idx As Integer) As CFStringRef
        Dim aKey As String = objectAtIndexS(keyAry, i)
        if InStr(aKey,"-VT-FS")>0 then
          flg=true
          exit
        end if
      next
      
      return flg
    End Function
    
  4. Window1のTextArea1の、Lockingプロパティのうち、下ロックを解除。
  5. 以下をWindow1にペースト(できなければ、Sub - End Subの間をResizedイベントハンドラに記述)
    Sub Resized() Handles Resized
      TextArea1.Height=me.Height-(kTopMargin+kBottomMargin)
      gWin1Height=gGetWinHeight(me.Handle)
    End Sub
    
  6. 以下をWindow1にペースト(できなければ、Sub - End Subの間をResizingイベントハンドラに記述)
    Sub Resizing() Handles Resizing
      TextArea1.Height=me.Height-(kTopMargin+kBottomMargin)
    End Sub
    
  7. 以下をGlobalsにペースト(できなければ定数に、名前:kBottomMargin、データ型:Number、デフォルト値:20、を追加)
    Public Const kBottomMargin as Number = 20
    
  8. 以下をGlobalsにペースト(できなければ定数に、名前:kTabbarHeight、データ型:Number、デフォルト値:24、を追加)
    Public Const kTabbarHeight as Number = 24
    
  9. 以下をGlobalsにペースト(できなければ定数に、名前:kToolbarHeight、データ型:Number、デフォルト値:78、を追加)
    Public Const kToolbarHeight as Number = 78
    
  10. 以下をGlobalsにペースト(できなければ定数に、名前:kTopMargin、データ型:Number、デフォルト値:20、を追加)
    Public Const kTopMargin as Number = 20
    
  11. 以下をGlobalsにペースト
    Public Function gGetWinHeight(winHndl As Integer) as Integer
      // Xojoのwindow.Heightは、タブバーの有無で値が変わってしまって使い勝手が悪いので、Cocoa側から取得する
      
      // 指定されたウィンドウのフレームサイズを取得
      Declare Function frame Lib "Cocoa" Selector "frame" (receiver As Integer) As NSRect
      Dim frame As NSRect = frame(winHndl)
      
      // フレームの高さからツールバーの高さ(実測値)を引く
      Dim h As Integer = frame.h-kToolbarHeight
      
      // ウィンドウの正味の高さを返す
      return h
    End Function
    
  12. 以下をGlobalsにペースト(できなければプロパティに、名前:gTabShow、データ型:Boolean、を追加)
    Public Property gTabShow as Boolean
    
  13. 以下をGlobalsにペースト(できなければプロパティに、名前:gWin1Height、データ型:Integer、を追加)
    Public Property gWin1Height as Integer
    
  14. 新規クラス(名前は、ここでは「NSNotificationCenter」)を作成。
  15. 以下をNSNotificationCenterにペースト
    Private Shared Function GetCurrentTab() as Variant
      // 現在選択されているタブ(ウィンドウ)を取得(Cocoa側)
      Declare Function selectedWindow Lib "Cocoa" Selector "selectedWindow" (receiver As Ptr) As Ptr
      Dim win1 As Variant = selectedWindow(gTabGroup)
      
      // 開いている全てのウィンドウをチェック(Xojo側)
      for i As Integer = 0 to WindowCount-1
        #if Target32Bit
          if win1.IntegerValue=window(i).Handle then  // ハンドルが一致すれば
            return window(i)  // ウィンドウを返す
          end if
        #endif
        #if Target64Bit
          if win1.Int64Value=window(i).Handle then  // ハンドルが一致すれば
            return window(i)  // ウィンドウを返す
          end if
        #endif
      next
    End Function
    
  16. 以下をNSNotificationCenterにペースト
    Private Shared Sub getPost(id As Ptr, SEL As CString, notify As Ptr)
      // notifyが無効なら戻る
      if notify=nil then return
      
      // notifyからuserInfoを取得
      Declare Function userInfo Lib "Cocoa" Selector "userInfo" (receiver As Ptr) As Ptr
      Dim info As Ptr = userInfo(notify)
      if info=nil then return  // userInfoが無効なら戻る
      
      // userInfoから値(メニュー項目)を取得
      Declare Function objectForKey Lib "Cocoa" Selector "objectForKey:" (receiver As Ptr, key As CFStringRef) As Ptr
      Dim menuItem As Ptr = objectForKey(info, "MenuItem")
      if menuItem=nil then return  // メニュー項目が無効なら戻る
      
      // メニュー項目からメニュー名を取得
      Declare Function title Lib "Cocoa" Selector "title" (receiver As Ptr) As CFStringRef
      Dim name As String = title(menuItem)
      
      // ウィンドウサイズ補正
      if name="タブバーを表示" then
        Dim win1 As Window1 = GetCurrentTab()  // 最前面のタブ(ウィンドウ)を取得
        win1.TextArea1.Height=gWin1Height-kTabbarHeight-(kTopMargin+kBottomMargin)+1  // +1は現物合わせ
        gTabShow=true
      end if
      if name="タブバーを非表示" then
        Dim win1 As Window1 = GetCurrentTab()  // 最前面のタブ(ウィンドウ)を取得
        win1.TextArea1.Height=gWin1Height-(kTopMargin+kBottomMargin)
        gTabShow=false
      end if
    End Sub
    
  17. 以下をNSNotificationCenterにペースト
    Public Shared Sub InitCenter(notName As String)
      // 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。
      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 NotifyObserver <> nil then
        return
      end if
      
      // クラス名をmyNotif、メタクラス名をNSObjectにして、生成
      Dim newClassId As Ptr = objc_allocateClassPair(NSClassFromString("NSObject"), "myNotif", 0)
      // ランタイムに登録(参照を可能とするため)
      objc_registerClassPair newClassId
      // 通知の受け口となるメソッドを追加(getPost:をXojo側で用意したgetPostメソッドで受け取る。)
      Dim nSelector As Ptr = NSSelectorFromString("getPost:")
      if not class_addMethod (newClassId, nSelector, AddressOf getPost, "@@:@") then
        msgBox "error."
        return
      end if
      
      // 上記で生成したクラスのインスタンスを作成
      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 targetId As Ptr = init(alloc(newClassId))
      
      // インスタンスを保持
      NotifyObserver = targetId
      
      // defaultCenterを取得
      Dim nCenter As Ptr = NSClassFromString("NSNotificationCenter")
      Declare Function defaultCenter Lib "Cocoa" Selector "defaultCenter" (receiver As Ptr) As Ptr
      nCenter = defaultCenter(nCenter)
      
      // defaultCenterにObserver/Selector/名前/オブジェクトをセット
      Declare Sub addObserver Lib "Cocoa" Selector "addObserver:selector:name:object:" (receiver As Ptr, obs As Ptr,sel As Ptr, nam As CFStringRef, obj As Ptr)
      addObserver(nCenter, NotifyObserver, nSelector, notName, nil)
    End Sub
    
  18. 以下をNSNotificationCenterにペースト(できなければ共有プロパティに、名前:NotifyObserver、データ型:Ptr、を追加)
    Private Shared Property NotifyObserver as Ptr
    
  19. 他に、NSRect(構造体)が必要ですが、macoslibからコピーさせて頂きました。(上記Globalsまたは別途モジュールを用意してコピーする。)
    注)macoslibではNSRectのメンバーの型にSingleが割り当てられているが、64bitにも対応したい場合は、CGFloatに書き換える。
 実行してみたところ、タブバーの表示/非表示に応じて、ウィンドウの内容物がリサイズされることを確認しました。


 おわりに

 あくまで対症療法的なものなので、ケースの組み合わせによっては不具合が生じるかもしれません。更なる検証が必要かと思われます。

 あと、内容物の高さが、タブバー表示では1ピクセルずれることが確認されていますので、現物会わせで対応はしているものの、こちらも原因調査が必要かと思われます。


 お世話になったサイト

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


 更新履歴

 2019.08.15 新規作成


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