タイトル【SnapXShot制作記】挫折か…と思ったらうまくいったCarbon Event化カテゴリーCarbon/CF, SnapXShot制作記
作成日2001/1/26 17:38:13作成者新居雅行
Mac OS Xで画面ショットを直接ファイルに落としていくユーティリティを作ろうと決心を固め、その過程をお届けするこのシリーズは、約2ヶ月ぶりくらいになるだろうか。元は、Appleが配付しているサンプルプログラムに画面のピクチャを作成するものがあったので、それを改造すればOKだと考え、とりあえず、ちょっと不安定な面もあるけど、Command+Shift+3で、Documentsフォルダに画像ファイルが連番で残るようなものができ、公開した。Mac FanやASCIIなどの雑誌に掲載されたりと、それなりの反響はあったという次第だ。

そこで次のステップに行くことを考えた。当初のプログラムは、昔のEventモデルであるため、キーセンスは旧来のポーリング手法だ。つまり、何もイベントがないときには逐一キー状態をチェックして…という方法である。こうしたイベント処理はマルチタスク処理の障害になるため、Mac OS Xでよりスムーズに稼動する(はずの)Carbon Event Managerが用意された。いずれはこっちで動作させる必要があるわけだから、とにかく手をつけておきたい。幸い、MDOnlineでは【小池邦人のプログラミング日記】で、小池さんがCarbon Eventについてたっぷりと解説してくれているので、すぐにでもプログラミングにとりかかることができた。
ShapXShotはまだユーザインタフェースがないので、メニューやウインドウのハンドリングをしなくてもいい。後は、キーの状態を逐一チェックするために、タイマーイベントを発行して、そこでキーの状態をチェックするようにすればOKなはずだ。楽勝じゃないの…。

だけど、まずはアプリケーションの基本である「終了できるようにする」を組み込まないといけない(笑)。これも小池さんの記事を読む限りは、簡単にできそうだ。当然ながら、基本イベントのQuitに対応するハンドラはすでにある。まずはイベントループをやめて、初期化の後にRunApplicationEventLoop(); を呼び出す。後は、コールバックルーチンだけの世界に入る。で、(悪い癖だが〜笑)いきなり実行…終了できない…。当然で、内部的なイベント処理をやめないとアプリケーションは終了しないのである。QuitイベントでのハンドラにQuitApplicationEventLoop(); という呼び出しを追加しなければならない。これで、Command+Qや、Quitのメニュー項目の処理は完結する。ちなみに、よくやっていた手段で、Quitイベントハンドラ内でグローバル変数に値を指定して、それをイベントループでチェックして終了するというようなことはもはや必要なくなる。

さて、いよいよタイマーイベントの組み込みだ。実はこうしたことも想定して、「現在の画面をPICTファイルに保存する」という関数をすでに作ってある。タイマーイベントで呼び出されるコールバックルーチンから、その用意してある関数を呼び出せばいいはずだ。まず、そのコールバックルーチンを定義通りに作成する。ちなみに、以下のような形式であればよい。

void MyTimeEventHandler(
EventLoopTimerRef timerRef,
void* userData)
{ .... }

この関数を、一定時間ごとに呼び出すハンドラは初期化の段階で、以下のような方法でシステムに登録できる。該当部分のダイジェストになるが、次の通りだ。

EvetnLoopTimeRef timerRef;
EventTimerInterval interval;

interval = 0.1; //呼び出し間隔は秒数で指定する
anErr = InstallEventLoopTimer(
GetCurrentEventLoop(),
0,
interval,
NewEventLoopTimerUPP(MyTimeEventHandler),
NULL,
&timerRef);

間隔を調整したりしながら、システム警告音をタイマー呼び出し関数で使うなどして、確かに一定時間ごとにイベントが発生して、MyTimeEventHandler関数を呼び出すのは確認した。
そして、MyTimeEventHandlerから、画面ショットをPICTファイルに落とす関数を呼び出してみた。だが全然動かないのである。エラー処理はいいかげんではあるものの、何のエラーもなく、落ちることもなく、ただただ静かに何もしない。キーセンスされたことを示すシステム警告音だけが空しく鳴るだけだ。

もちろん、デバッガを動かしてトレースしてみた。どうも、グラフィックス関連ではエラーは出ていない。グラフィックス処理はほぼQuickDrawなので、あまりエラーは見つけにくいが、Picture構造体はとにかく、それなりに埋まっている…。ここでふと思い出した、Carbon Eventによるコールバックは割り込みルーチンである。Interrupt-safeなAPIを記述した以下のTechnoteを思い出した。

◇Interrupt-Safe Routines
 http://developer.apple.com/technotes/tn/tn1104.html

ただ、この文書では、コールバックルーチンはinterrupt routineに含まれていない。イベントのコールバックルーチンや、Drag Managerのルーチンについては記載されておらず、デバイス関連のコールなどがinterrupt routineに入るものとして説明されている(なお、この文書ではOpen TransportはInterrupt-Safeのリストに含まれている…)。
これを読む限りでは、QuickDrawにはInterrupt-Safeなものはないと書かれている。だとしたら、ウインドウ処理などはできないことになってしまうから、Carbon Eventのコールバックはまた別物なのじゃないかと思うが、それなりに参考にはなるかもしれないので、使っているマネージャを逐一チェックをした。そのなかで、File Managerの記述が目に入った。ファイル処理は割り込みルーチンでは非同期のものしか使えないため、FSなんとかのAPIはだめだというのである。
SnapXShotでは、ファイル名を決める部分で、決めたファイル名のものが存在すれば、ファイル名に含まれる番号を増加させて、いずれにしても存在しないファイル名を決定するという部分がある。ここでは、FSSpec構造体を作り、その戻り値でファイルが存在するかを判断している。はたして…デバッガで追ったところ、どうも、FSMakeFSSpec関数の戻り値がおかしい。存在するファイルを指定したのに、存在することを示す戻り値が戻らないのである。既存のファイルと同じ名前のものが決定された場合であっても、既存の画像ファイルに上書きすることになるはずなのだが、その上書きもされたりされなかったりする。もしかして、ハイレベルのFile Managerが機能していないのではないかということなのだろうか。それで、PBMakeFSSpecAsyncなどとやってみたのだが、慣れない低レベルAPIでちょっと疲れてしまった。とにかくうまく行かない。

……と思っていたが、原稿を書いてから改めてTechnoteを見ていると違うポイントに目が向いた。いやはや、危ない危ない(原稿を書き直そうとしたけど、まあ、低レベルのプログラマはこんなもんだということがあってもいいかと思うので、続けて書こう)。どうやら、グローバル変数というものは今一つ怪しいようなのだ。そういえば、ユーザデータをコールバックルーチンに引き渡すくらいだから、グローバルは使うなということなのではないだろうか。そう考えれば確かにそう思うわけだが、果たして…やはりそうだった…。
実は、Documentsフォルダのボリューム参照番号とディレクトリIDをグローバル変数に入れていたのだが、その値が、コールバック内では違うのになっている。すっかりこういうところは信用し切っていたから目が回らないものなのだ。改めてコールバックルーチン内でローカル変数にボリューム参照番号とディレクトリIDを代入してやれば、見事に画面ショットはファイルに収まったという次第だ。
他にもグローバル変数は少し使っているのだが、そこはたまたま支障はなかったのだろう。ここもきちんとユーザデータ化して構造体で管理をして、プログラムを整理してからリリースをしようかと思う。
関連リンクSnapXShot