ホームページ開発ツール>Xojo / Real Studio / REALbasic コラム

Xojo / Real Studio / REALbasic コラム


 ○Xojo 2022r4.1
 ○Xojo 2021r2.1
 ○Xojo 2018r2 多言語版
 ○Xojo 2017r1.1 以前のコラムはこちら
 DeclareのBlocksにObjCBlockを使う(ネイティブ&Cocoa)

 Blocksは、既にCocoaのDeclareでファイルIOパネルを拡張する・プラグイン使わないCocoaのDeclareでMIDIを鳴らすで話題にしています。
 ですが、多少の進展がありましたので、(両者とも特に問題なく機能するので、記事の書き換えは行わずに)こちらに纏めておくことにしました。

 Blocksの正体は構造体とのことで、両記事は構造体ベースで試行しましたが、2019r2からObjCBlockが新設され、Xojoネイティブの機能として扱えるようになりました。
 ObjCBlockの使い方はシンプルで、Blocksの内容を記述したメソッドを、インスタンス生成時に引数として渡すだけです。

 CocoaのDeclareでファイルIOパネルを拡張する・プラグイン使わないの構造体を置き換えるには、「Xojoでの実装」2項を以下の通りとします。
// シート生成(ObjCBlock)
Declare Sub beginSheetModalForWindow Lib "Cocoa" Selector "beginSheetModalForWindow:completionHandler:" (receiver As Ptr, win As Integer, block As Ptr)
Dim blks As new ObjCBlock(AddressOf didEndSheetCH)
beginSheetModalForWindow(Panel, win.Handle, blks.Handle)

 CocoaのDeclareでMIDIを鳴らすでは、実験的に導入はしてみたものの、StackOverflowExceptionが発生して使えなかったので別アプローチにしましたが、最近になって以下に気づきました。
 参考サイト:Audio buffer - Targets / iOS - Xojo Programming Forum
(注:記事の初出時より古い投稿なので、気づいてもよかった筈ですが、見逃していました…。)

 確かにpragmaを指定すれば通ることを確認しましたが、なにがどうなってそういうことになるのかは把握できていません。
(念の為、Leaksでチェックしてみましたが、確認した範囲でメモリーリークはありませんでした。)
 また、音量の取得もプロパティとかではなく、自分で計算しなければいけないようなので、ここでは枠組みのみの記述としておきます。
 なお、installTapOnBus:bufferSize:format:block:のBlocksは別スレッドになるので、コントロールにアクセスすると異常終了します。この辺は、XojoネイティブのThreadと同じ対策が必要になります。

 「Xojoでの実装」29項の末尾にある「// エンジン実行」の直前に、以下を追加。
// タップのセット(音量計算のためのストリーム取得用)
Dim blks As ObjCBlock = new ObjCBlock(AddressOf tapBlock)
Declare Sub installTapOnBus Lib "Cocoa" selector "installTapOnBus:bufferSize:format:block:" (class_id As Ptr, bus As UInteger, size As UInt32, format As Ptr, block As Ptr)
installTapOnBus(pMixer, 0, 1024, format, blks.Handle)
 以下をContainerControl1にペースト
Private Sub tapBlock(buffer As Ptr, when As Ptr)
  #pragma StackOverflowChecking false
  #Pragma BackgroundTasks false
  
  // 音量計算
  
End Sub
(注:音量計算は、例えば記事でも触れた右記サイトが参考になります。:ios - Level Metering with AVAudioEngine - Stack Overflow

 書き方は構造体よりスマートで、動作は(一工夫必要な場合もあるが)安定しているので、2019r2以降ではこちらを使った方が良さそうです。
 戻る  先頭へ


 Declareで構造体の配列を送受する(Cocoa)

 過去に、構造体の配列を引数に指定する必要に迫られた時は結局分からずじまいで、別の方法を取らざるを得なかったのですが、ようやく分かってきました。
 まずは受信の方ですが、以下にありました。(実はこのトピック自体は以前から知っていたのですが、構造体という視点では見ていませんでした。節穴か?<自分の目)
 参考サイト:API returning a C Array - how to read Objc.protocol_copymethoddescriptionList? - macOS - Xojo Programming Forum
 送信の方も探したのですが、(探し方が悪かったのか)見つからなかったので、受信方法から類推して試行錯誤の結果、なんとかいけるようになりました。

 以下は、サンプルです。
Public Sub PutGetStructureArray()
  // 以下を再現
  'objc_property_attribute_t type = { "T", "@\"NSString\"" };
  'objc_property_attribute_t ownership = { "C", "" }; // C = copy
  'objc_property_attribute_t backingivar  = { "V", "_privateName" };
  'objc_property_attribute_t attrs[] = { type, ownership, backingivar };
  
  // 構造体の配列作成
  Dim mb As new MemoryBlock(3 *attr.Size)  // 要素数は、3
  Dim mbp As Ptr = mb
  mbp.attr(0 *attr.Size).name="T"
  mbp.attr(0 *attr.Size).value="@""NSString"""
  mbp.attr(1 *attr.Size).name="C"
  mbp.attr(1 *attr.Size).value=""
  mbp.attr(2 *attr.Size).name="V"
  mbp.attr(2 *attr.Size).value="_privateName"
  
  // 構造体の配列をプットしたい場合は、mbpをPtr型の引数として渡す。
  
  Dim mbx As Ptr = mbp  // 構造体の配列をゲットしたい場合は、Ptr型の変数で受ける。 // 値がmbxにセットされて戻ってきた状態を想定
  
  // 構造体の配列から要素を取得
  for i As Integer = 0 to 2
    Dim desc As attr = mbx.attr(i *attr.Size)
    Dim nam As String = desc.name
    Dim val As String = desc.value
    
    MessageBox str(i)+" : name="+nam+" , value="+val  // デバッグ用
  next
End Sub
Structure attr
  name As CString
  value As CString
End Structure
 戻る  先頭へ


 ファイル保存ダイアログの拡張子オプションを機能させる(別版)(Cocoa)

 二個下の方法は、2021r2.1では有効なのですが、一方で、2018r2では機能が不十分ということも確認できています。
 即ち、2018r2では、設定の変更が反映されるのは次回オープン時で、変更直後には反映されない、という形になります。

 そこで、再度色々実験してみたのですが、どれもうまくいきませんでした。
(例えば、このオプションはplistに保持されるので、そこから読み出してみたのですが、結果はこちらも変更直後には反映されない、というものでした。)
 が、ふと「次回には反映されるのだから、クローズ直後に再度ダイアログを生成(ただし、表示はしない)してやれば、そこから取得できるのでは?」と思ってやってみたところ、うまくいきました。

 この方法はシンプルで、2021r2.1でも有効です。
 また、生成だけして表示しないというのも、特に問題はなさそうです。(Leaksでの検証では問題ありませんでした。)

 以下は、二個下のmyExtensionVisible()を置き換えるサンプルです。
Public Function myExtensionVisible() as Boolean
  // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
  Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
  
  // SavePanelの生成(生成だけで、表示はしない)
  Dim panel As Ptr = NSClassFromString("NSSavePanel")
  Declare Function savePanel Lib "Cocoa" selector "savePanel" (class_id As Ptr) As Ptr
  panel = savePanel(panel)
  
  // 拡張子表示フラグの取得
  Declare Function isExtensionHidden Lib "Cocoa" Selector "isExtensionHidden" (receiver As Ptr) As Boolean
  Dim flg As Boolean = not isExtensionHidden(panel)  // XojoとCocoaでは考え方が逆なので、値を反転して保持
  
  return flg
End Function
 戻る  先頭へ


 クリエーター/タイプを復活させる(Cocoa)

 Xojoでは2019.2以降、標準でクリエーター/タイプを扱えなくなりました。
 新規に開発する場合はそれでいいと思いますが、古来から継ぎ足し継ぎ足しで伝統の味を守っているプロジェクトでは困ったことになりました。
 一方、CocoaのAPIはDeprecatedになることもなく、普通に使えていますので、こちらを使った代替処理を考えてみました。

 以下はクリエーター用のサンプルですが、NSFileHFSCreatorCodeNSFileHFSTypeCodeに置き換えれば、そのままタイプにも適用できます。
Public Function GetCreator(f As FolderItem) as String
  #if XojoVersion>2019.1  // 2019.2 or Later
    // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
    Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
    
    // FileManagerを取得
    Dim fm As Ptr = NSClassFromString("NSFileManager")
    Declare Function defaultManager Lib "Cocoa" Selector "defaultManager" (receiver As Ptr) As Ptr
    fm = defaultManager(fm)
    // ファイルからアトリビュートを取得
    Declare Function attributesOfItemAtPath Lib "Cocoa" Selector "attributesOfItemAtPath:error:" (receiver As Ptr, path As CFStringRef, err As Ptr) As Ptr
    Dim dic As Ptr = attributesOfItemAtPath(fm, f.NativePath, nil)
    
    // Creatorを取得
    Declare Function objectForKeyStr Lib "Cocoa" Selector "objectForKey:" (receiver As Ptr, key As CFStringRef) As Ptr
    Dim pnt As Ptr = objectForKeyStr(dic, "NSFileHFSCreatorCode")  // TypeはNSFileHFSTypeCode
    if pnt=nil then return ""  // 取得できなければ空を返す
    
    // NSNumberをIntegerに変換
    Declare Function unsignedIntegerValue Lib "Cocoa" Selector "unsignedIntegerValue" (receiver As Ptr) As UInt32
    Dim ivv As UInt32 = unsignedIntegerValue(pnt)
    // FourCC ("four-character code")を文字列に変換
    Declare Function NSFileTypeForHFSTypeCode Lib "Cocoa" (ostype As UInt32) As CFStringRef
    Dim stt As String = NSFileTypeForHFSTypeCode(ivv)
    
    if stt="''" then return ""  // 未設定なら空を返す
    return MidB(stt,2,4)  // 設定済なら文字部分だけ返す
    
  #else  // 2019.1 or Earlier
    return f.MacCreator
  #endif
End Function
Public Sub SetCreator(f As FolderItem, cre As String)
  #if XojoVersion>2019.1  // 2019.2 or Later
    // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
    Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
	
    // FileManagerを取得
    Dim fm As Ptr = NSClassFromString("NSFileManager")
    Declare Function defaultManager Lib "Cocoa" Selector "defaultManager" (receiver As Ptr) As Ptr
    fm = defaultManager(fm)
	
    // 文字列をFourCC ("four-character code")に変換(NSHFSTypeCodeFromFileTypeは不可っぽい?ので、ここではベタな方法で生成)
    Dim c1 As UInt32 = Asc(MidB(cre,1,1))
    Dim c2 As UInt32 = Asc(MidB(cre,2,1))
    Dim c3 As UInt32 = Asc(MidB(cre,3,1))
    Dim c4 As UInt32 = Asc(MidB(cre,4,1))
    Dim oint As UInt32 = Bitwise.ShiftLeft(c1, 24)+Bitwise.ShiftLeft(c2, 16)+Bitwise.ShiftLeft(c3, 8)+c4
    // IntegerをNSNumberに変換
    Dim numb As Ptr = NSClassFromString("NSNumber")
    Declare Function numberWithUnsignedInteger Lib "Cocoa" Selector "numberWithUnsignedInteger:" (receiver As Ptr, val As UInt32) As Ptr
    numb = numberWithUnsignedInteger(numb, oint)
	
    // Creatorを含むアトリビュートを生成
    Dim dict As Ptr = NSClassFromString("NSDictionary")
    Declare Function dictionaryWithObject Lib "Cocoa" Selector "dictionaryWithObject:forKey:" (receiver As Ptr, num As Ptr, key As CFStringRef) As Ptr
    dict = dictionaryWithObject(dict, numb, "NSFileHFSCreatorCode")  // TypeはNSFileHFSTypeCode
	
    // ファイルにセット
    Declare Sub setAttributes Lib "Cocoa" Selector "setAttributes:ofItemAtPath:error:" (receiver As Ptr, attr As Ptr, path As CFStringRef, err As Ptr)
    setAttributes(fm, dict, f.NativePath, nil)
	
  #else  // 2019.1 or Earlier
    f.MacCreator=cre
  #endif
End Sub
 戻る  先頭へ


 ファイル保存ダイアログの拡張子オプションを機能させる(Cocoa)

 以前は出来ていたと思うのですが、いつの頃からか、ファイル保存ダイアログで指定した拡張子表示オプションが、作成したファイルに反映されず、常に拡張子が表示されるようになっています。
 ファイルのオプションは自分でセットできるので、ダイアログから情報が得られればいいのですが、Xojo側からはファイル保存ダイアログ自身(handle)にも、拡張子表示オプションプロパティにもアクセスはできないようです。

 そこで、実験してみたところ、XojoのSaveFileDialogNSSavePanelをそのまま使っていることが確認できました。
 となれば、ファイル保存ダイアログを同時に複数個表示することはない、という前提で、表示中の全ウィンドウ/ダイアログからNSSavePanelを探し出し、そこからextensionHiddenプロパティを取得、という方法が使えます。

 以下は、XojoのLanguage ReferenceのSaveFileDialogのサンプルを一部抜粋したものに、当該処理を追加したものです。
…
saveFile = dlg.ShowModal
If saveFile <> Nil Then
  // saveFile is the FolderItem of the file to save
  
  // ここに、ファイル保存処理を記述
  
  // ファイルの拡張子表示/非表示をセット
  saveFile.ExtensionVisible=myExtensionVisible()
  
Else
  // user canceled
End If
 以下は、myExtensionVisible()のサンプルです。
Protected Function myExtensionVisible() As Boolean
  // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
  Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
  // クラスオブジェクトを指定して文字列を取得する。最初に一回宣言しておけばよい。
  Declare Function NSStringFromClass Lib "Cocoa" (aClass As Ptr) As CFStringRef
  
  Declare Function objectAtIndex Lib "Cocoa" Selector "objectAtIndex:" (receiver As Ptr, info As Integer) As Ptr
  Declare Function myClass Lib "Cocoa" Selector "class" (receiver As Ptr) As Ptr
  
  // NSApplicationクラスのsharedApplicationを取得
  Dim sA As Ptr = NSClassFromString("NSApplication")
  Declare Function SharedApplication Lib "Cocoa" Selector "sharedApplication" (receiver As Ptr) As Ptr
  sA = SharedApplication(sA)
  
  // 現在表示中のウィンドウ/ダイアログを全て取得
  Declare Function windows Lib "Cocoa" Selector "windows" (receiver As Ptr) As Ptr
  Dim wins As Ptr = windows(sA)
  
  // 配列の個数の取得
  declare function count lib "Cocoa" selector "count" (receiver As Ptr) as Integer
  Dim cnt As Integer = count(wins)
  
  // 表示中のウィンドウ/ダイアログのループ
  Dim flg As Boolean = false  // デフォルトはここではfalse
  Dim win, cls As Ptr
  for i As Integer = 0 to cnt-1
    win = objectAtIndex(wins, i)  // 順番に取得
    cls = myClass(win)  // オブジェクトからクラスを取得
    if NSStringFromClass(cls)="NSSavePanel" then  // ファイル保存ダイアログなら
      // extensionHiddenを取得
      Declare Function isExtensionHidden Lib "Cocoa" Selector "isExtensionHidden" (receiver As Ptr) As Boolean
      flg = not isExtensionHidden(win)  // XojoとCocoaでは考え方が逆なので、値を反転して保持
      exit  // 抜ける
    end if
  next
  
  return flg
End Function
 戻る  先頭へ


 PICTリソースを取得する【10.15以降】(Xcode)

 リソースフォークの取得は既にこちらで行なっていますが、この方法では、Catalina以降はPICTリソースの取得ができないことが分かっています。
(PICTリソースからNSImageを作成すると、TIFFRepresentationでエラーが発生する。Xcode 12で確認。)
 一方、PICTファイルは問題ないことが分かったため、PICTリソースの先頭に512バイトの0を追加してみたところ、正常に処理しました。

 以下は、「リソースフォークの実装」5項のif ([kind isEqualToString:@"PICT"]) {部分を置き換えるサンプルです。
if ([kind isEqualToString:@"PICT"]) {
	NSMutableData* data2 = [[[NSMutableData alloc] initWithLength:512] autorelease];
	[data2 appendBytes:[data bytes] length:[data length]];
	destFile = [NSString stringWithFormat:@"%d.tiff", theID];
	destPath = [destSubFolder stringByAppendingPathComponent:destFile];
	NSImage* img = [[[NSImage alloc] initWithData: data2] autorelease];
	[[img TIFFRepresentation] writeToFile:destPath atomically:YES];
}
 戻る  先頭へ


 システム利用のボリューム以外をリストアップする(Cocoa)

 ボリューム(SSD/HDDやDVD、USBメモリー等)の取得はXojoネイティブの機能(Volume())でできるのですが、APFSの場合は、システムのみが利用するボリュームもピックアップされます。
 これらは(デフォルトでは)Finderに表示されないので、除外したい場合もあるかと思います。
 Volume()の戻り値はFolderItemなので、そこから非表示の情報を得ようとすると名前ぐらいしかなく(Visibleはtrueが返ってくる)、ちょっと弱い気もします。

 一方、CocoaのAPIでは、NSFileManagerのmountedVolumeURLsIncludingResourceValuesForKeys:options:で、optionsにNSVolumeEnumerationSkipHiddenVolumes(=2)を指定すると、所望の結果が得られます。
 参考サイト:日暮れて道遠し: Cocoa NSTreeControllerを調べる(4)
 全てと表示のみを切り替えたい場合は、その都度optionsを変更して再取得する以外に、optionsなしで取得後、NSURLVolumeIsBrowsableKeyで判別する方法もある。
 以下は、ボリュームのパスを取得して、配列に格納して返すサンプルです。(mode=0 or 2で、切り替え可)
Private Function GetVolume(mode As Integer) as String()
  // mode : 0 = all , 2 = NSVolumeEnumerationSkipHiddenVolumes
  // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
  Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
  
  Dim fm As Ptr = NSClassFromString("NSFileManager")
  Declare Function defaultManager Lib "Cocoa" Selector "defaultManager" (receiver As Ptr) As Ptr
  fm = defaultManager(fm)
  
  Declare Function mountedVolumeURL Lib "Cocoa" Selector "mountedVolumeURLsIncludingResourceValuesForKeys:options:" (receiver As Ptr, keys As Ptr, opt As Integer) As Ptr
  Dim urls As Ptr = mountedVolumeURL(fm, nil, mode)
  
  declare function count lib "Cocoa" selector "count" (receiver As Ptr) as Integer
  Dim cnt As Integer = count(urls)
  
  Declare Function objectAtIndex Lib "Cocoa" Selector "objectAtIndex:" (receiver As Ptr, info As Integer) As Ptr
  Declare Sub getResourceValue Lib "Cocoa" Selector "getResourceValue:forKey:error:" (receiver As Ptr, val As Ptr, key As CFStringRef, err As Ptr)
  Declare Function UTF8String Lib "Cocoa" Selector "UTF8String" (receiver As Ptr) As CString
  
  Dim url As Ptr
  Dim paramS As New MemoryBlock(8)
  Dim st As String
  Dim err As New MemoryBlock(8)
  Dim volPath(-1) As String
  for i As Integer = 0 to cnt-1
    url = objectAtIndex(urls, i)
    
    getResourceValue(url, paramS, "_NSURLPathKey", err)  // 文字列で指定する場合は_NSURLPathKeyになる
    if paramS.Ptr(0)<>nil then
      st = UTF8String(paramS.Ptr(0))  // 文字列がUTF-8形式で格納される
      st = DefineEncoding(st, Encodings.UTF8)  // エンコーディングがUTF-8であることを明示(しないと、パスが日本語を含んでいる場合に文字化けする)
      volPath.Append st
    end if
  next
  
  return volPath
End Function
 ボリュームの中身にアクセスしたい場合は、GetFolderItem(pathArray(n),3)でOKです。

 あと、Time Machineディスクも除外したい場合は、上記APIではダメで、別途tmutil destinationinfoで取得する、等の必要がありそうです。
(以下はターミナルでの実行例。NameよりMount Pointを使った方が確実か。)
hoge$ tmutil destinationinfo
====================================================
Name          : Time Machine HD
Kind          : Local
Mount Point   : /Volumes/Time Machine HD
ID            : XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
 参考サイト:macos - Is there a reliable way to know a volume is a time machine volume when using the disk arbitration framework? - Stack Overflow

 追記:macos 10.15(Catalina)以前では、FinderにはUpdateという名前のボリュームが表示されますが、上記サンプルでは同様に表示されます。データにアクセスしたい場合は、HDD名 - Data(またはHDD名 - データ)が適切かと思われますが、取得方法については未調査です。(2021/03/16)
 追記2:macos 10.14(Mojave)では、HDD名 - Data(またはHDD名 - データ)も表示されます。それ以前のOSは未調査です。バージョンごとの対応が必要になるかも…。(2021/04/05)
 戻る  先頭へ


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