ホームページ>開発ツール>Xojo / Real Studio Trial and Error・CocoaのDeclareで環境設定パネルをOS標準っぽくしてみる(改訂版)
Xojo / Real Studio Trial and Error
目次
CocoaのDeclareで環境設定パネルをOS標準っぽくしてみる(改訂版)
はじめに
以下は、Xojo Cocoaビルドについての話題です。
環境設定のためのパネルを、OS添付のアップル製アプリケーションが用いている方法で使えるかどうか、調べてみました。
なお検証には、Xojo 2016 Release 3を用いています。(Mac mini mid 2010 + OS X 10.12.6 Sierra)
注1)本件はパネルの表示に関してのものであり、値のセットや変更の反映等については対象としていません。
注2)アップルのガイドラインによると「Restore the last viewed preference pane.」なので、対応したところ、ソースコードの改変量が増えてしまったので、改訂版としました。
参考サイト(3):Preferences - App Architecture - Human Interface Guidelines for macOS Apps
方針
アップル純正アプリの環境設定の特徴(の一つ)は、ページの内容によってサイズが変わり、その際にアニメーションを伴う点でしょう。
確認できた限り、パネルの形式には二種類あるようです。
殆どのアプリ(以下、標準)はウィンドウを使い、唯一(?)の例外がiTunesで、ダイアログを使っています。
前述のガイドラインでは、ウィンドウを使え、となっています。両者には、アニメーションの仕方にも違いがあります。目視で確認した限り、
標準:まず、現在のページのコントロールが不可視になり、リサイズのアニメーションが実行された後、切り替わったページのコントロールとタイトルが表示される。
iTunes:まず、ページが切り替わり、コントールとタイトルが表示された後、リサイズのアニメーションが実行される。
アニーメーションの実装については、何種類かあるようですが、ウィンドウについては、以下のサイトがそのものなので大変参考になります。
参考サイト(1):(旧) Cocoaの日々: 内容によってウィンドウのサイズを変える (2) NSViewAnimation
一方ダイアログは、ウィンドウと同じでもいいのですが、こちらは終了通知を特に必要としないので、NSAnimationContextを使ってみることにしました。
以上を踏まえ、それぞれの仕様は以下の通りとしました。
例1(ウィンドウ形式)
例2(ダイアログ形式)
- アニメーションのクラスは、NSViewAnimationを使う
- アニメーションの終了通知はDelegateで受け取るので、使い勝手を考えて、クラス化しておく
共通
- アニメーションのクラスは、NSAnimationContextを使う
- アニメーションのメソッドはモジュールに置く
- ツールバーはXojoネイティブを使う
- ページの切り替えには、ページパネルを使う
- パネルを閉じた時のページ番号は、ファイルに保存する等して次回に継承すべきだが、今回のテーマではないので省略。
Xojoでの実装(例1)
【ソースコードのコピー&ペーストについて】
ソースコード(グレー背景部分の全文)をコピーし、指定のウィンドウ/クラスにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
ただし、この方法は、メソッドでは問題ないようですが、イベント/アクション/プロパティでは不安定?なので、ペーストできない場合は、各項目のカッコ内を適用して下さい。
実行してみたところ、アニメーションが機能することを確認しました。
- Xojoで新規プロジェクトを作成
- 以下をAppにペースト(できなければ、Function - Endの間をAppPrefsメニューハンドラに記述)
Function AppPrefs() As Boolean Window2.Show Return True End Function
- MainMenuBar>編集メニューの最後に、項目(Name:AppPrefs、Super:PrefsMenuItem、Text:環境設定...)を追加
注1)SuperがPrefsMenuItemであれば、どのメニューに追加してもOKなようです。
注2)最後の文字に三点リーダを使うか、ピリオド3つ使うかについては、例えば以下のサイトが参考になります。
参考サイト(2):ユーザインターフェイスの語句にこだわる - Qiita(Human Interface Guidelinesの項へジャンプ)- 新規ツールバーを作成(名前は、ここでは「Toolbar2」とした。)
- Toolbar2にツールアイテムを3個追加(Caption:項目1/項目2/項目3、Style:Toggle Button)
注)アイコンは適当な画像を別途用意します。(なくても、アニメ機能の確認はできます。)- 新規ウィンドウを作成(名前は、ここではデフォルトの「Window2」とした。)
- Window2にページパネルをドラッグし、Panelsの編集ボタンを押して、ページを追加(Page 0〜Page 3の4ページ)
- Window2にToolbar2をドラッグ(インスタンスの名前はToolbar21になる)
- 以下をToolbar21にペースト(できなければ、Sub - Endの間をActionイベントに記述)
Sub Action(item As ToolItem) Handles Action // 現在選択中のボタンが押されたら、ページ移動せず、トグルもしない Dim idx As Integer select case item.Caption case "項目1" idx=1 case "項目2" idx=2 case "項目3" idx=3 end select if idx=PanelValue then select case item.Caption case "項目1" me.ToolItem1.Pushed=true case "項目2" me.ToolItem2.Pushed=true case "項目3" me.ToolItem3.Pushed=true end select return end if // ページ移動(アニメあり) MovePage(item.Caption,true) End Sub
- 以下をWindow2にペースト(できなければ、Sub - Endの間をCloseイベントに記述)
Sub Close() Handles Close // 現在のページ番号を保持 PageNo=PagePanel1.Value End Sub
- 以下をWindow2にペースト(できなければ、Sub - Endの間をOpenイベントに記述)
Sub Open() Handles Open // NSViewAnimeクラスのインスタンス生成 viewAnime = new NSViewAnime(AddressOf animationDidEnd) // ページ番号に対応するツールアイテムを押下状態に PushButtonItem(PageNo) // ページ番号からキャプションを取得 Dim capt As String select case PageNo case 1 capt="項目1" case 2 capt="項目2" case 3 capt="項目3" end select // ページ移動(アニメなし) MovePage(capt,false) End Sub
- 以下をWindow2にペースト
Private Sub animationDidEnd() // ページパネル切り替え PagePanel1.Value=PanelValue // タイトルをセット select case PanelValue case 1 self.Title="項目1" case 2 self.Title="項目2" case 3 self.Title="項目3" end select End Sub
- 以下をWindow2にペースト
Private Sub MovePage(capt As String, anime As Boolean) // ページ移動 select case capt case "項目1" PagePanel1.Value=0 PanelValue=1 ResizeWindow(self,600,400,anime) case "項目2" PagePanel1.Value=0 PanelValue=2 ResizeWindow(self,600,200,anime) case "項目3" PagePanel1.Value=0 PanelValue=3 ResizeWindow(self,600,300,anime) end select End Sub
- 以下をWindow2にペースト
Private Sub PushButtonItem(itemNo As Integer) // ページ番号に対応するツールアイテムを押下状態に select case itemNo case 1 Toolbar21.ToolItem1.Pushed=true case 2 Toolbar21.ToolItem2.Pushed=true case 3 Toolbar21.ToolItem3.Pushed=true end select End Sub
- 以下をWindow2にペースト
Private Sub ResizeWindow(win As Window, endW As Integer, endH As Integer, anime As Boolean) // アニメ判定 if anime then viewAnime.DoViewAnime(win,endW,endH+78) // 高さに78足しているのはツールバーの高さ(cocoaの処理にはXojoのツールバー高さが反映されないようだ) else win.Width=endW win.Height=endH animationDidEnd() // アニメーション終了後に呼ばれる処理を直ちに行う end if End Sub
- 以下をWindow2にペースト(できなければプロパティに、名前:PanelValue、データ型:Integer、を追加)
Private Property PanelValue as Integer
- 以下をWindow2にペースト(できなければプロパティに、名前:viewAnime、データ型:NSViewAnime、を追加)
Private Property viewAnime as NSViewAnime
- 新規モジュールを作成(名前は、ここでは「Globals」とした。)
- 以下をGlobalsにペースト(できなければプロパティに、名前:PageNo、データ型:Integer、標準値:1、を追加)
Public Property PageNo as Integer = 1
- 新規クラスを作成(名前は、ここでは「NSViewAnime」とした。)
- 以下をNSViewAnimeにペースト(できなければ移譲に、名前:ActionDelegate、を追加)
Private Sub ActionDelegate()
- 以下をNSViewAnimeにペースト
Public Sub Constructor(action As ActionDelegate) ActionHandler = action // クラス生成元でActionを受け取るメソッドを登録 End Sub
- 以下をNSViewAnimeにペースト
Public Sub DoViewAnime(win As Window, endW As Integer, endH As Integer) // 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr Declare Function NSSelectorFromString Lib "Cocoa" (aSelName As CFStringRef) As Ptr // ウィンドウのポインタを取得するため、まずhandleからviewを取得して、その後window(ポインタ)を取得する Declare Function contentView Lib "Cocoa" selector "contentView" (class_id As Integer) As Ptr Dim pnt1 As Ptr = contentView(win.handle) Declare Function myWindow Lib "Cocoa" selector "window" (class_id As Ptr) As Ptr Dim winPtr As Ptr = myWindow(pnt1) Declare Function frame Lib "Cocoa" Selector "frame" (receiver As Ptr) As NSRect Dim startFrom As NSRect = frame(winPtr) Dim endAt As NSRect = NSMakeRect(startFrom.x, startFrom.y+(startFrom.h-endH), endW, endH) Dim duration As Double = 0.4 // Singleではダメ Dim dict As Ptr = NSClassFromString("NSMutableDictionary") Declare Function dictionary Lib "Cocoa" Selector "dictionary" (receiver As Ptr) As Ptr dict = dictionary(dict) Declare Sub setObject Lib "Cocoa" Selector "setObject:forKey:" (receiver As Ptr, obj As Ptr, key As CFStringRef) setObject(dict, winPtr, "NSViewAnimationTargetKey") Dim valu1 As Ptr = NSClassFromString("NSValue") Declare Function valueWithRect Lib "Cocoa" Selector "valueWithRect:" (receiver As Ptr, rect As NSRect) As Ptr valu1 = valueWithRect(valu1, startFrom) setObject(dict, valu1, "NSViewAnimationStartFrameKey") Dim valu2 As Ptr = NSClassFromString("NSValue") valu2 = valueWithRect(valu2, endAt) setObject(dict, valu2, "NSViewAnimationEndFrameKey") Dim ary As Ptr = NSClassFromString("NSArray") Declare Function arrayWithObject Lib "Cocoa" Selector "arrayWithObject:" (receiver As Ptr, obj As Ptr) As Ptr ary = arrayWithObject(ary, dict) Dim anim As Ptr = NSClassFromString("NSViewAnimation") Declare Function alloc Lib "Cocoa" Selector "alloc" (receiver As Ptr) As Ptr anim = alloc(anim) Declare Function initWithViewAnimations Lib "Cocoa" Selector "initWithViewAnimations:" (receiver As Ptr, obj As Ptr) As Ptr anim = initWithViewAnimations(anim, ary) Declare Sub setDuration Lib "Cocoa" Selector "setDuration:" (receiver As Ptr, value As Double) setDuration(anim, duration) Declare Sub setDelegate Lib "Cocoa" Selector "setDelegate:" (receiver As Ptr, id As Ptr) setDelegate(anim, makeDelegate()) // Delegateの設定 Declare Sub startAnimation Lib "Cocoa" Selector "startAnimation" (receiver As Ptr) startAnimation(anim) Declare Sub release Lib "Cocoa" Selector "release" (receiver As Ptr) release(anim) End Sub
- 以下をNSViewAnimeにペースト
注)SELの型をPtrとしていたが、CStringの方が相応しいので変更した。(未使用なので変更しなくても実害はなし。)(2018.07.30)Private Shared Sub animationDidEnd(id As Ptr, SEL As CString, animation As Ptr) ActionHandler.Invoke() // クラス生成元でActionを受け取るメソッドを呼び出す End Sub
- 以下をNSViewAnimeにペースト
Private Shared Function makeDelegate() as Ptr // 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。 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 delegateId<>nil then return delegateId end if // クラス名をmyNSViewAnimationDelegate(名前は任意。少なくとも今回のケースでは参照されない。)、メタクラス名をNSObjectにして、生成 Dim newClassId As Ptr = objc_allocateClassPair(NSClassFromString("NSObject"), "myNSViewAnimationDelegate", 0) // ランタイムに登録(参照を可能とするため) objc_registerClassPair newClassId // Delegateの対象となるメソッドを追加(animationDidEnd:をXojo側で用意したanimationDidEndメソッドで受け取る。) if not class_addMethod (newClassId, NSSelectorFromString("animationDidEnd:"), AddressOf animationDidEnd, "v@:@@") then msgBox "error." return nil 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 delegateId = init(alloc(newClassId)) // インスタンスを返す return delegateId End Function
- 以下をNSViewAnimeにペースト(できなければ共有プロパティに、名前:ActionHandler、データ型:ActionDelegate、を追加)
Private Shared Property ActionHandler as ActionDelegate
- 以下をNSViewAnimeにペースト(できなければ共有プロパティに、名前:delegateId、データ型:Ptr、を追加)
Private Shared Property delegateId as Ptr
- 他に、NSMakeRect(メソッド)、NSRect(構造体)が必要ですが、それらはmacoslibからコピーさせて頂きました。(上記Globalsにコピーする。)
Xojoでの実装(例2)
実行してみたところ、アニメーションが機能することを確認しました。
- Xojoで新規プロジェクトを作成
- 以下をAppにペースト(できなければ、Function - Endの間をAppPrefsメニューハンドラに記述)
Function AppPrefs() As Boolean Window2.ShowModal Return True End Function
- MainMenuBar>編集メニューの最後に、項目(Name:AppPrefs、Super:PrefsMenuItem、Text:環境設定...)を追加
注1)SuperがPrefsMenuItemであれば、どのメニューに追加してもOKなようです。
注2)最後の文字に三点リーダを使うか、ピリオド3つ使うかについては、例えば以下のサイトが参考になります。
参考サイト(2):ユーザインターフェイスの語句にこだわる - Qiita(Human Interface Guidelinesの項へジャンプ)- 新規ツールバーを作成(名前は、ここでは「Toolbar2」とした。)
- Toolbar2にツールアイテムを3個追加(Caption:項目1/項目2/項目3、Style:Toggle Button)
注1)アイコンは適当な画像を別途用意します。(なくても、アニメ機能の確認はできます。)
注2)ボタン群の両脇にFlexible Spaceを置くと、よりiTunesっぽくなります。- 新規ウィンドウを作成(名前は、ここではデフォルトの「Window2」)し、TypeをMovable Modalにする。
- Window2にページパネルをドラッグし、Panelsの編集ボタンを押して、ページを追加(Page 0〜Page 3の4ページ)
注)本方式では、空のページは不要なので、3ページでも可。(その場合は、コードを適宜変更してください。)- Window2にデフォルトボタンとキャンセルボタンを(ページパネルの外に)置き、以下をペースト(できなければ、Sub - Endの間をActionイベントに記述)
注)ボタンが押された後の処理は、今回のテーマではないので省略。Sub Action() Handles Action Hide End Sub
- Window2にToolbar2をドラッグ(インスタンスの名前はToolbar21になる)
- 以下をToolbar21にペースト(できなければ、Sub - Endの間をActionイベントに記述)
Sub Action(item As ToolItem) Handles Action // 現在選択中のボタンが押されたら、ページ移動せず、トグルもしない Dim idx As Integer select case item.Caption case "項目1" idx=1 case "項目2" idx=2 case "項目3" idx=3 end select if idx=PagePanel1.Value then select case item.Caption case "項目1" me.ToolItem1.Pushed=true case "項目2" me.ToolItem2.Pushed=true case "項目3" me.ToolItem3.Pushed=true end select return end if // ページ移動(アニメあり) MovePage(item.Caption,true) End Sub
- 以下をWindow2にペースト(できなければ、Sub - Endの間をOpenイベントに記述)
Sub Open() Handles Open // ページ番号に対応するツールアイテムを押下状態に PushButtonItem(PageNo) // ページ番号からキャプションを取得 Dim capt As String select case PageNo case 1 capt="項目1" case 2 capt="項目2" case 3 capt="項目3" end select // ページ移動(アニメなし) MovePage(capt,false) End Sub
- 新規モジュールを作成(名前は、ここでは「Globals」とした。)
- 以下をGlobalsにペースト
Public Sub DoAnime(win As Window, endW As Integer, endH As Integer) // 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr Declare Function NSSelectorFromString Lib "Cocoa" (aSelName As CFStringRef) As Ptr Declare Function contentView Lib "Cocoa" selector "contentView" (class_id As Integer) As Ptr Dim view As Ptr = contentView(win.handle) Declare Function frame Lib "Cocoa" Selector "frame" (receiver As Integer) As NSRect Dim frame1 As NSRect = frame(win.handle) Dim startFrom As NSRect = NSMakeRect(frame1.x, frame1.y, frame1.w, frame1.h) Dim endAt As NSRect = NSMakeRect(frame1.x, frame1.y+(frame1.h-endH), endW, endH) Dim duration As Double = 0.4 // Singleではダメ Declare Sub setFrameInt Lib "Cocoa" Selector "setFrame:display:" (receiver As Integer, frame As NSRect, disp As Boolean) setFrameInt(win.handle, startFrom, false) Dim cntx As Ptr = NSClassFromString("NSAnimationContext") Declare Sub beginGrouping Lib "Cocoa" Selector "beginGrouping" (receiver As Ptr) beginGrouping(cntx) Dim cntx2 As Ptr = NSClassFromString("NSAnimationContext") Declare Function currentContext Lib "Cocoa" Selector "currentContext" (receiver As Ptr) As Ptr cntx2 = currentContext(cntx2) Declare Sub setDuration Lib "Cocoa" Selector "setDuration:" (receiver As Ptr,durn As Double) setDuration(cntx2, duration) Declare Function animator Lib "Cocoa" Selector "animator" (receiver As Integer) As Ptr Dim anim As Ptr = animator(win.handle) Declare Sub setFramePtr Lib "Cocoa" Selector "setFrame:display:" (receiver As Ptr, frame As NSRect, disp As Boolean) setFramePtr(anim, endAt, true) Declare Sub endGrouping Lib "Cocoa" Selector "endGrouping" (receiver As Ptr) endGrouping(cntx) End Sub
- 以下をGlobalsにペースト(できなければプロパティに、名前:PageNo、データ型:Integer、標準値:1、を追加)
Public Property PageNo as Integer = 1
- 他に、NSMakeRect(メソッド)、NSRect(構造体)が必要ですが、それらはmacoslibからコピーさせて頂きました。(上記Globalsにコピーする。)
おわりに
アニメーションの滑らかさは特に問題ないと思われますが、ページやコントロールが増えたり、バックグラウンド処理の多いアプリなんかでのパフォーマンスは未知数です。
あと、iTunesだけがダイアログなのは何故なんでしょうね。
お世話になったサイト
貴重な情報をご提供頂いている皆様に、お礼申し上げます。(以下、順不同)
参考サイト(1):(旧) Cocoaの日々: 内容によってウィンドウのサイズを変える (2) NSViewAnimation
参考サイト(2):ユーザインターフェイスの語句にこだわる - Qiita(Human Interface Guidelinesの項へジャンプ)
参考サイト(3):Preferences - App Architecture - Human Interface Guidelines for macOS Apps
更新履歴
2018.07.30 Xojoでの実装(例1)の24項を改訂
2017.11.06 実装の項のソースコードを大幅に変更したので、改訂版とした。
2017.11.06 はじめにと方針にガイドラインの説明を追加し、方針の「共通」の最後に項目を追加
2017.10.27 新規作成
[Home] [MacSoft] [Donation] [History] [Privacy Policy] [Affiliate Policy]