高度な検索
Developer Connection
Member Login ログイン | ご入会 ADC連絡先

Technote 1147

Pending Update Perils


 

目次

はじめに

更新と ModalDialog

問題を回避する具体的な方法

さらに必要となる処理

結論

のテクニカルノートでは、モーダルダイアログの背後にあるウインドウに対して更新イベントのペンディングが提供されていないときに起こりうる問題について説明します。

はじめに

モーダルダイアログボックスはこれまで、その背後にあるウインドウに関連したいくつかの問題の原因になってきました。ModalDialog は、デフォルトの設定でアプリケーションのイベントループと協調的に動作しない独自のイベントループを持っているため、ModalDialog ループに入っているときには、アプリケーション内の他のウインドウに更新が発生したことを正しく認識できない可能性があります。

モーダルダイアログに対するフィルタプロシージャを書いた経験のあるデベロッパなら、おそらくこの問題をすでに経験しているはずです。フィルタは更新イベントの断続的なストリームを取得します。これらのイベントはダイアログに対するものではなく、ダイアログの背後にあるウインドウに対するものであり、これらのウインドウはモーダルダイアログが最前面にあるため更新されません。イベントは通常のイベントループ経由で処理されません。また、ダイアログに対するイベントのみにかかわっているため、おそらく更新を提供することもないはずです。これにより、更新イベントが再送信され続けるという問題が発生します。更新を停止するただ 1 つの方法は、影響を受けるウインドウの更新領域を描画ルーチンの Begin/EndUpdate 呼び出しによってクリアすることです (『Inside Macintosh:Mactintosh Toolbox Essentials』の「Handling Update Events」を参照してください)。

この状況は、スクリーンセーバーやバルーンヘルプによってさらに悪化します。モーダルダイアログが表示されているときにスクリーンセーバーがアクティブになったり、ユーザがバルーンヘルプを使っていて、ダイアログの背後にあるウインドウの一部がバルーンによって隠される場合、ウインドウに対する更新イベントが生成されることになります。

先頭ページに戻る

 

更新と ModalDialog

アプリケーションに対する更新イベントのペンディングが存在する場合、他のアプリケーション、ドライバ、コントロールパネル、あるいはその他のプロセスは時間を取得しなくなります。

他のアプリケーションに対する更新のペンディングは一般に問題の原因とはなりません (それらもまた更新のペンディングの影響を受けていないかぎり)。これらの更新は、バックグラウンドのアプリケーションによって通常通りに処理されます。更新を提供しなければ、他のプロセスは時間を取得しなくなります。

このことが潜在的な問題の原因になる可能性があります。コードの多くの部分は、正常に動作したり、ネットワーク接続を維持したり、あるいは正常な画面表示を行うために時間を必要とします。

単純な例は「セレクタ」です。「セレクタ」を選択した後、モーダルダイアログを持つアプリケーションを起動してみてください。「セレクタ」を適切な位置に移動すれば、バックグラウンドにある間でも、そのリストが更新されることに気づくはずです。

ここで、最前面にあるアプリケーション内で書類ウインドウが開いていることを確認し、「ヘルプ」メニューの「バルーン表示」をオンにします。

アプリケーション内でモーダルダイアログを開きます (たいていのアプリケーションの「... について」ダイアログはモーダルダイアログとして動作します)。ここで、モーダルダイアログの背後にあるウインドウの上にカーソルを移動してみます。「画面上にダイアログがあるので、このウインドウは一番手前に表示されていません。...」などの情報を含むバルーンが表示され、ウインドウの一部がバルーンによって隠されます。ここで、「セレクタ」を見てください。「セレクタ」は実行を停止しています。バルーンによって攻撃を受けたウインドウはそれに対する更新のペンディングを行っており、その更新はプログラムのイベントループ経由ではなく、ModalDialog トラップ経由で実行されるため、更新は提供されません。その結果、他のすべてのアプリケーションに対して時間は停止します。

注意:
このような現象は、更新がダイアログボックスと同じアプリケーションに対するものである場合にのみ発生します。別のアプリケーション (Finder など) のウインドウをバルーンで隠している場合、その更新は正常に処理されます。

先頭ページに戻る

 

問題を回避する具体的な方法

このような現象の発生を避けるため、アプリケーション内でとりうる 2 つの選択肢があります。まず第 1 の選択肢は、モーダルダイアログを開くときにはアプリケーション内に他のオープンウィンドウを残さないという方法です。しかし、この選択肢が常に現実的な解決方法といえないことは明らかです。

より正しい第 2 の解決方法は、モーダルダイアログの内部からすべてのウィンドウの更新を行うようなメカニズムを自ら提供することです。

フィルタプロシージャ (『Inside Macintosh:Macintosh Toolbox Essentials』の「Writing an Event Filter Function for Alert and Modal Dialog Boxes」で説明されています) は、この問題の解決に使用できる適切なツールといえます。この場合、アプリケーション内に表示するすべてのダイアログまたはアラートに単純なフィルタプロシージャを追加する必要があります。しかも、大部分のケースでは、すべてのダイアログに同じフィルタを追加できるため、余分なコードを大量に書く必要もありません。

ただし、この選択肢を採用するときには若干の準備が必要になります。フィルタプロシージャは、任意のウインドウに対する描画プロシージャを呼び出す方法を持っていなければなりません。これを実現するため、アプリケーション独自の必要条件やデベロッパのプログラミングスタイルによって、さまざまな方法が存在します。描画ルーチンへのポインタを含むウインドウコントロールオブジェクトを作成してもかまいませんし、メインイベントループで行っているのと同じチェックとディスパッチを組み込んでもかまいません。また、最適であると思われる別の方法を使用することも可能です。

最もシンプルで基本的な方法は、次に示すように、ウインドウレコード refCon の中に描画プロシージャに対するフラグを組み込み、refCon の値に基づいた独自の描画ルーチンベクタを持つことです。

/* どこか別の場所で定義されているウインドウ描画プロシージャ */
Boolean MyDrawProc (WindowPtr windowToDraw) {
 Boolean returnVal = true;
  
 /* それ以前にウインドウに格納していた値を */
 /* オフにする */
 switch (GetWRefCon(windowToDraw)) {
  case kMyClipboard: /* 自分のクリップボードを描画する */
   DrawMyClip (windowToDraw);
   break;
  case kMyDocument: /* 書類の内容 */
   DrawMyDoc (windowToDraw);
   break;
  default: /* ウインドウの描画を避けるために、他のことは何も行わない */
   returnVal = false;
   /* これは自分に関係ない */
   break;
 }
  
 /* この戻り値は、フィルタ内部から Dialog Manager を */
 /* 呼び出したときに、更新を処理したかどうかを Dialog Manager に */
 /* 通知するために使用する。通常の使用 (つまり、メインイベントループ */
 /* の updateEvent への応答) では、この論理値は必要ないが、 */
 /* 特に何らかの障害になることもない */
  
 return (returnVal);
}

ウィンドウを作成するときにフラグをインストールします。

myWindowPtr = GetNewWindow (kMyWindowID, nil, (WindowPtr)-1);
SetWRefCon (myWindowPtr, (long)myDrawingProcFlag);

フィルタ内の更新処理は次のようになります。

/* 更新がダイアログボックスに対するものである場合、通常の ModalDialog は
必要に応じてダイアログの再描画を行うため、この更新を無視する */
if(theEventIn->what == updateEvt && theEventIn->message != myDialogPtr ) {
 /* 独自の描画ルーチンに入る。対象となるウインドウを所有している場合は、
 そのウインドウが再描画されることになる */
 return (MyDrawProc ((WindowPtr)theEventIn->message));
}

MPW Pascal の場合

{ この関数の戻り値は、フィルタ内部から Dialog Manager を呼び出したときに、 }
{ 更新を処理したかどうかを Dialog Manager に通知するために使用する。 }
{ 通常の使用 (つまり、メインイベントループの updateEvent への応答) では、 }
{ この論理値は必要ないが、特に何らかの障害になることもない。 }
{ ウインドウの再描画プロシージャはどこか別の場所で定義されている }
 
FUNCTION MyDrawProc(windowToDraw WindowPtr): BOOLEAN;
 
BEGIN
 CASE GetWRefCon(windowPtr) OF
  
  kMyClipboard:
  BEGIN
   DrawMyClipboard(windowToDraw);
   MyDrawProc := TRUE;
  END;
   
  kMyDocument:
  BEGIN
   DrawMyDocument(windowToDraw);
   MyDrawProc := TRUE;
  END;
   
  OTHERWISE
   MyDrawProc := FALSE;
 END; {CASE}
END;

ウィンドウを作成するときにフラグをインストールします。

myWindowPtr := GetNewWindow(kMyWindowID, NIL, WindowPtr(-1));
SetWRefCon(myWindowPtr, myDrawingProcFlag);

フィルタ内の更新処理は次のようになります。

FUNCTION MyFilter(currentDialog: DialogPtr; VAR theEventIn: EventRecord;
VAR theItem: INTEGER): BOOLEAN;
 
{ 更新がダイアログボックスに対するものである場合、通常の ModalDialog 関数は }
{ 必要に応じてダイアログの再描画を行うため、この更新を無視する }
 
BEGIN
 
 IF (theEventIn.what = updateEvt AND theEventIn.message <> currentDialog)
  BEGIN
   MyFilter := MyDrawProc(currentDialog);
  END;
END;

先頭ページに戻る

 

さらに必要となる処理

独自のフィルタプロシージャをダイアログに追加することに対する唯一の問題は、デベロッパが更新だけでなくその他の処理も行うことを Dialog Manager が前提にしてしまう点にあります。特に Dialog Manager では、デベロッパが標準的な「item 1 への return キーエイリアス」のフィルタ処理を実行していると想定します。このため、デベロッパはキー操作の処理を自らフィルタ内に書かなければならなくなります。

System 7 の Dialog Manager には、この状況でプログラムに対する負荷を軽減する、いくつかの新しい呼び出しが用意されています。これらの呼び出しは、System 7 を開発する最終段階で作成されてテストされたため、『Inside Macintosh』には詳細に記述されていません。このような理由から『Inside Macintosh』を補足する目的で「Technote 1148: Dialog Manager のヘルパ関数」が用意されています。これらの関数を使用すると、System のサービスを呼び出して、ダイアログ内での標準的なキー操作を追跡することができます。

System 6 での処理方法

もちろん、System 7 より前のシステムに対応したアプリケーションでは、これらの新しい呼び出しを使用できないため、デベロッパ自身がすべての処理を行う必要があります。次に、System 7 のフィルタとほぼ同じ処理を行う System 6.0.x に対応したフィルタプロシージャのサンプルを示します。

/* System 7 より前のシステムに対応したダイアログ フィルタ */
#define kMyButtonDelay 8
 
/* ツールボックスによって呼び出されるため、'pascal' として宣言する */
pascal Boolean MyFilter (DialogPtr currentDialog,
EventRecord *theEventIn, short *theDialogItem) {
 Boolean returnVal = false;
 long waitTicks;
 short itemKind; /* GetDItem で使用するいくつかのテンポラリ変数 */
 Handle itemHandle;
 Rect itemRect;
  
 if (theEventIn->what == updateEvt && theEventIn->message != myDialogPtr) {
  /* myDialogPtr はダイアログを作成した場所で定義されている。
  更新がダイアログボックスに対するものである場合、通常の ModalDialog は
  必要に応じてダイアログの再描画を行うため、この更新を無視する */
   
  returnVal = MyDrawProc (theEventIn->message); /* 独自の描画ルーチンに入る */
 } else {
  /* それが更新でなければ、キー操作であったかどうかを確認する。return または enter キー
  をチェックし、item 1 としてそのエイリアスを作成する。また、ここには item 2 のエイリアス
  を作成する escape キーに対するチェックも組み込んでいるが、必ずしもこれを使用する必要は
  ない */
   
  if ((theEventIn->what == keyDown) || (theEventIn->what == autoKey)) {
   /* それはキーだった */
    
   switch (theEventIn->message & charCodeMask) {
    case kReturnKey:
    case kEnterKey:
    *theDialogItem = ok; /* 現在の項目が何であっても "OK" 項目に変更する。
                          ok は Dialogs.h で定義されている。
                          このときは、ユーザが正しいフィードバックを得られるように、
                          ボタンを反転させる必要がある */
     GetDItem (currentDialog, ok, &itemKind, &itemHandle, &itemRect);
     HiliteControl ((ControlHandle)itemHandle, inButton); /* ボタンを反転させる */
     Delay (kMyButtonDelay, &waitTicks); /* これが見えるように約 8 ティック待機する */
     HiliteControl ((ControlHandle)itemHandle, false); /* その後、通常の状態に戻る */
     
     returnVal = true; /* このイベントを処理したことを Dialog Manager に通知する */
     break;
         
    /* これは、escape キーに item 2 (通常は "キャンセル" ボタン) と同じフィルタ処理を行う */
    case kEscKey:
     *theDialogItem = cancel; /* cancel は Dialogs.h で 2 と定義されている */
     GetDItem (currentDialog, cancel, &itemKind, &itemHandle, &itemRect);
     HiliteControl ((ControlHandle)itemHandle, inButton);
     Delay (kMyButtonDelay, &waitTicks); /* これが見えるように約 8 ティック待機する */
     HiliteControl ((ControlHandle)itemHandle, false);
      
     returnVal = true; /* このイベントを処理したことを Dialog Manager に通知する */
     break;
   }
  }
 }
  
 return (returnVal);
}

MPW Pascal の場合

{ System 7 よりも前のシステムに対応したフィルタは次のようになる }
 
FUNCTION MyFilter(currentDialog: DialogPtr; VAR theEventIn:
 EventRecord; VAR theItem: INTEGER): BOOLEAN;
CONST
 kMyButtonDelay = 8;
 VAR
 itemKind : INTEGER;
 itemHandle : Handle;
 itemRect : Rect;
 savePort : GrafPtr;
 waitTicks : LONGINT;
 
BEGIN
 { 更新がダイアログボックスに対するものである場合、通常の ModalDialog 関数は }
 { 必要に応じてダイアログの再描画を行うため、この更新を無視する }
 IF (theEventIn.what = updateEvt AND theEventIn.message <> currentDialog)
  MyFilter := MyDrawProc(theEventIn.message)
 ELSE { it wasn't an update, see if it was a keystroke }
 BEGIN
  { return または enter キーをチェックし、"ok" 項目としてそのエイリアスを作成する }
  { ここには "キャンセル" 項目のエイリアスとなる escape キーに対するチェックも }
  { 組み込んでいるが、必ずしもこれを使用する必要はない }
  IF ((theEventIn.what = keyDown) OR (theEventIn.what = autoKey))
   BEGIN { それはキーだった }
    
   CASE CHR(BAnd(theEventIn.message, charCodeMask)) OF
    
    kReturnKey, kEnterKey:
     BEGIN
      GetDItem(currentDialog, ok, itemKind, itemHandle, itemRect);
      HiliteControl(ControlHandle(itemHandle), TRUE);
      Delay(kMyButtonDelay , waitTicks); { これが見えるように約 8 ティック待機する }
      HiliteControl(ControlHandle(itemHandle), FALSE); { その後、通常の状態に戻る }
      MyFilter := TRUE; { このイベントを処理したことを Dialog Manager に通知する }
     END;
     
    kEscKey:
     BEGIN
      theItem := cancel;
      GetDItem(currentDialog, cancel, itemKind, itemHandle, itemRect);
      HiliteControl(ControlHandle(itemHandle), TRUE);
      Delay(kMyButtonDelay , waitTicks); { これが見えるように約 8 ティック待機する }
      HiliteControl(ControlHandle(itemHandle), FALSE); { その後、通常の状態に戻る }
      MyFilter := TRUE; { このイベントを処理したことを Dialog Manager に通知する }
     END;
    
   END; {CASE}
  END;
 END;
END;

先頭ページに戻る

 

結論

果てしない更新は何も新しい問題ではありませんが、果てしない更新について何らかの対処を行うことが重要です。とはいえ、この問題に関してそれほど余分な作業が発生するわけではありません。すべてのダイアログおよびアラートに単純なフィルタを追加し、ウインドウ構造体の中に描画プロシージャへのフラグを置くだけです。

その結果、システムは円滑に実行を継続するはずです。また、結果的に、ユーザは常に、一部が欠けたウインドウではなく、本来の方法で表示されたアプリケーションのウインドウを見ることになります。

参考文献

改訂の履歴

  • 1991年10月、このテクニカルノートの初版が「Technote TB 37」として公開されました。
  • 1991年と1999年にサンプルコードの追加と改訂が行われました。
  • 1999年1月、提示したアイデアをよりよく整理するため、このテクニカルノートの内容は更新されました。

先頭ページに戻る