これを十分に明確に説明できることを願っています。メイン フォーム (A) があり、form.Show() を使用して 1 つの子フォーム (B) を開き、form.Show() を使用して 2 番目の子フォーム (C) を開きます。ここで、子フォーム B が form.ShowDialog() を使用してフォーム (D) を開くようにします。これを行うと、フォーム A とフォーム C もブロックされます。モーダル ダイアログを開いて、それを開いたフォームのみをブロックする方法はありますか?
11 に答える
複数の GUI スレッドを使用するのは難しいビジネスです。これが唯一の動機である場合は、使用しないことをお勧めします。
より適切な方法は、Show()
代わりにを使用ShowDialog()
し、ポップアップ フォームが戻るまでオーナー フォームを無効にすることです。考慮事項は次の 4 つだけです。
を使用する
ShowDialog(owner)
と、ポップアップ フォームはその所有者の上にとどまります。を使用する場合も同様ですShow(owner)
。Owner
または、同じ効果でプロパティを明示的に設定することもできます。所有者フォームの
Enabled
プロパティをに設定するfalse
と、フォームは無効な状態を示します (子コントロールは「グレー表示」されます)。一方、ShowDialog
を使用すると、所有者フォームは引き続き無効になりますが、無効な状態は表示されません。を呼び出す
ShowDialog
と、オーナー フォームは Win32 コードで無効になり、そのWS_DISABLED
スタイル ビットが設定されます。これにより、フォーカスを取得する機能が失われ、クリックしたときに「音を立てる」ことができなくなりますが、それ自体が灰色になるわけではありません。フォームの
Enabled
プロパティをfalse
に設定すると、追加のフラグが設定されます (基礎となる Win32 サブシステムではなく、フレームワーク内で) 特定のコントロールが自身を描画するときにチェックします。このフラグは、無効な状態で自分自身を描画するようにコントロールに指示するものです。したがって、 で何が起こるかをエミュレートするには、フォームのプロパティをに設定するのではなく
ShowDialog
、ネイティブWS_DISABLED
スタイル ビットを直接設定する必要があります。これは、ちょっとした相互運用によって達成されます。Enabled
false
const int GWL_STYLE = -16; const int WS_DISABLED = 0x08000000; [DllImport("user32.dll")] static extern int GetWindowLong(IntPtr hWnd, int nIndex); [DllImport("user32.dll")] static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); void SetNativeEnabled(bool enabled){ SetWindowLong(Handle, GWL_STYLE, GetWindowLong(Handle, GWL_STYLE) & ~WS_DISABLED | (enabled ? 0 : WS_DISABLED)); }
ダイアログが閉じられるまで、
ShowDialog()
呼び出しは返されません。ダイアログが処理を完了するまでオーナー フォームのロジックを一時停止できるため、これは便利です。Show()
呼び出しは、必ずしもこのようには動作しません。したがって、Show()
の代わりにを使用するShowDialog()
場合は、ロジックを 2 つの部分に分割する必要があります。ダイアログが閉じられた後に実行されるコード (所有者フォームの再有効化を含む) は、Closed
イベント ハンドラーによって実行される必要があります。フォームがダイアログとして表示されている場合、その
DialogResult
プロパティを設定すると、フォームが自動的に閉じます。DialogResult
このプロパティは、以外のプロパティを持つボタンがクリックされるたびに設定されNone
ます。で示されているフォームShow
はこのように自動的に閉じないため、閉じるボタンの 1 つがクリックされたときに明示的に閉じる必要があります。ただし、DialogResult
プロパティはボタンによって適切に設定されることに注意してください。
これら 4 つのことを実装すると、コードは次のようになります。
class FormB : Form{
void Foo(){
SetNativeEnabled(false); // defined above
FormD f = new FormD();
f.Closed += (s, e)=>{
switch(f.DialogResult){
case DialogResult.OK:
// Do OK logic
break;
case DialogResult.Cancel:
// Do Cancel logic
break;
}
SetNativeEnabled(true);
};
f.Show(this);
// function Foo returns now, as soon as FormD is shown
}
}
class FormD : Form{
public FormD(){
Button btnOK = new Button();
btnOK.DialogResult = DialogResult.OK;
btnOK.Text = "OK";
btnOK.Click += (s, e)=>Close();
btnOK.Parent = this;
Button btnCancel = new Button();
btnCancel.DialogResult = DialogResult.Cancel;
btnCancel.Text = "Cancel";
btnCancel.Click += (s, e)=>Close();
btnCancel.Parent = this;
AcceptButton = btnOK;
CancelButton = btnCancel;
}
}
別のスレッドを使用することもできますが (以下のように)、これは危険な領域に入りつつあります。スレッド化の意味 (同期、クロススレッド アクセスなど) を理解している場合にのみ、このオプションに近づく必要があります。
[STAThread]
static void Main() {
Application.EnableVisualStyles();
Button loadB, loadC;
Form formA = new Form {
Text = "Form A",
Controls = {
(loadC = new Button { Text = "Load C", Dock = DockStyle.Top}),
(loadB = new Button { Text = "Load B", Dock = DockStyle.Top})
}
};
loadC.Click += delegate {
Form formC = new Form { Text = "Form C" };
formC.Show(formA);
};
loadB.Click += delegate {
Thread thread = new Thread(() => {
Button loadD;
Form formB = new Form {
Text = "Form B",
Controls = {
(loadD = new Button { Text = "Load D",
Dock = DockStyle.Top})
}
};
loadD.Click += delegate {
Form formD = new Form { Text = "Form D"};
formD.ShowDialog(formB);
};
formB.ShowDialog(); // No owner; ShowDialog to prevent exit
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
};
Application.Run(formA);
}
(明らかに、上記のように実際にコードを構成することはありません。これは、動作を示す最短の方法にすぎません。実際のコードでは、フォームごとにクラスを作成します。)
フォーム B を A および C とは別のスレッドで実行すると、ShowDialog 呼び出しはそのスレッドのみをブロックします。明らかに、それはもちろん仕事への些細な投資ではありません。
別のスレッドで Form D の ShowDialog 呼び出しを実行するだけで、ダイアログがどのスレッドもブロックしないようにすることができます。これには同じ種類の作業が必要ですが、アプリのメイン スレッドから実行されるフォームが 1 つだけになるため、作業ははるかに少なくなります。
可能な解決策を要約し、1 つの新しい選択肢 (3a および 3b) を追加したいと思います。しかし、最初に、私たちが話していることを明確にしたいと思います。
複数のフォームを持つアプリケーションがあります。フォームの特定のサブセットのみをブロックし、他の部分はブロックしないモーダル ダイアログを表示する必要があります。モーダル ダイアログは、1 つのサブセット (シナリオ A) または複数のサブセット (シナリオ B) でのみ表示できます。
そして今、可能な解決策の要約:
ShowDialog()
viaで表示されるモーダル フォームを使用しないアプリケーションの設計について考えてください。本当に
ShowDialog()
メソッドを使用する必要がありますか? モーダル フォームを使用する必要がない場合は、モーダル フォームが最も簡単でクリーンな方法です。もちろん、この解決策が常に適切であるとは限りません。私たちに与えるいくつかの機能があります
ShowDialog()
。最も注目に値するのは、所有者が無効になり (グレー表示にならない)、ユーザーが操作できないことです。非常に疲れる答えがP Daddyを提供しました。エミュレート
ShowDialog()
動作その数学の動作をエミュレートすることは可能です。繰り返しますが、P Daddy's answerを読むことをお勧めします。
a)
Enabled
プロパティ onForm
と show formの組み合わせを、を介して非モーダルとして使用しShow()
ます。その結果、無効なフォームはグレー表示されます。ただし、相互運用を必要としない、完全に管理されたソリューションです。b)親フォームがグレー表示されたくないですか? いくつかのネイティブ メソッドを参照し、親フォームのビットをオフにし
WS_DISABLED
ます(もう一度 - P Daddyからの回答を参照してください)。これら 2 つのソリューションでは、処理する必要があるすべてのダイアログ ボックスを完全に制御する必要があります。「部分的にブロックしているダイアログ」を表示するには特別な構成を使用する必要があり、それを忘れてはなりません。
Show()
は非ブロッキングであり、ブロッキングであるため、ロジックを調整する必要がありますShowDialog()
。システム ダイアログ (ファイル チューザー、カラー ピッカーなど) の処理が問題になる可能性があります。一方、ダイアログによってブロックされないフォームに余分なコードは必要ありません。の限界を克服する
ShowDialog()
イベントがあること
Application.EnterThreadModal
に注意してください。Application.LeaveThreadModal
このイベントは、モーダル ダイアログが表示されるたびに発生します。イベントは実際にはスレッド全体であり、アプリケーション全体ではないことに注意してください。Application.EnterThreadModal
a)ダイアログによってブロックされないフォームでイベントをリッスンし、それらのフォームのビットをオンにします。WS_DISABLED
モーダル ダイアログによってブロックされるべきではないフォームを調整するだけで済みます。WS_DISABLED
表示されているモーダル フォームの親チェーンを検査し、この条件に基づいて切り替える必要がある場合もあります (例では、フォーム A と C でダイアログを開く必要があるが、フォーム B と D をブロックしない場合)。b) ブロックしてはならないフォームを非表示にして再表示します。モーダル ダイアログが表示された後に新しいフォームを表示すると、ブロックされないことに注意してください。それを利用して、モーダル ダイアログが表示されたら、目的のフォームを非表示にして再度表示し、ブロックされないようにします。ただし、このアプローチではちらつきが発生する場合があります。理論的には、Win API でフォームの再描画を有効または無効にすることで修正できますが、私はそれを保証しません。
c)
Owner
ダイアログが表示されたときにブロックされるべきではないフォームのダイアログフォームにプロパティを設定します。私はこれをテストしませんでした。d) 複数の GUI スレッドを使用します。TheSmurf からの回答。
FormA の新しいスレッドで FormB を開始します。
(new System.Threading.Thread(()=> {
(new FormB()).Show();
})).Start();
これで、ShowDialog() を使用して新しいスレッドで開かれたフォームは FormB のみをブロックし、FormA または FormC はブロックしません。
ここにソリューションを追加したかったのは、私にとってはうまくいくようで、単純な拡張メソッドにカプセル化できるからです。@nightcoderが@PDaddyの回答にコメントしたように、私がする必要があるのは点滅に対処することだけです。
public static void ShowWithParentFormLock(this Form childForm, Form parentForm)
{
childForm.ShowWithParentFormLock(parentForm, null);
}
public static void ShowWithParentFormLock(this Form childForm, Form parentForm, Action actionAfterClose)
{
if (childForm == null)
throw new ArgumentNullException("childForm");
if (parentForm == null)
throw new ArgumentNullException("parentForm");
EventHandler activatedDelegate = (object sender, EventArgs e) =>
{
childForm.Focus();
//To Do: Add ability to flash form to notify user that focus changed
};
childForm.FormClosed += (sender, closedEventArgs) =>
{
try
{
parentForm.Focus();
if(actionAfterClose != null)
actionAfterClose();
}
finally
{
try
{
parentForm.Activated -= activatedDelegate;
if (!childForm.IsDisposed || !childForm.Disposing)
childForm.Dispose();
}
catch { }
}
};
parentForm.Activated += activatedDelegate;
childForm.Show(parentForm);
}
私が書いていたアプリケーションで同様の問題に直面していました。私のメイン UI は、メイン スレッドで実行されるフォームでした。モードレス ダイアログとして実行したいヘルプ ダイアログがありました。これは簡単に実装でき、実行中のヘルプ ダイアログのインスタンスが 1 つだけであることを確認できました。残念ながら、私が使用したモーダル ダイアログはヘルプ ダイアログのフォーカスを失う原因にもなりました。
ここや他の場所で言及されているアイデアを使用して、このバグを克服することができました。
メイン UI 内でスレッドを宣言しました。
Thread helpThread;
次のコードは、ヘルプ ダイアログを開くために発生するイベントを処理します。
private void Help(object sender, EventArgs e)
{
//if help dialog is still open then thread is still running
//if not, we need to recreate the thread and start it again
if (helpThread.ThreadState != ThreadState.Running)
{
helpThread = new Thread(new ThreadStart(startHelpThread));
helpThread.SetApartmentState(ApartmentState.STA);
helpThread.Start();
}
}
void startHelpThread()
{
using (HelpDialog newHelp = new HelpDialog(resources))
{
newHelp.ShowDialog();
}
}
また、このコードを初めて実行するときに null オブジェクトを参照していないことを確認するために、コンストラクターに追加されたスレッドの初期化も必要でした。
public MainWindow()
{
...
helpThread = new Thread(new ThreadStart(startHelpThread));
helpThread.SetApartmentState(ApartmentState.STA);
...
}
これにより、スレッドが常に 1 つのインスタンスしか持たないことが保証されます。スレッド自体がダイアログを実行し、ダイアログが閉じられると停止します。別のスレッドで実行されるため、メイン UI 内からモーダル ダイアログを作成しても、ヘルプ ダイアログがハングすることはありません。追加する必要がありました
helpDialog.Abort();
メイン UI のフォーム クロージング イベントに追加して、アプリケーションの終了時にヘルプ ダイアログが確実に閉じるようにします。
メイン UI 内から生成されたモーダル ダイアログの影響を受けないモードレス ヘルプ ダイアログができました。これはまさに私が望んでいたものです。メイン UI とヘルプ ダイアログの間で必要な通信がないため、これは安全です。
おそらく、子ウィンドウ (詳細についてはChildWindowを参照) の方がより洗練されたソリューションであり、GUI の個別のスレッドに関するすべての問題を回避できます。
使用例:
(new NoneBlockingDialog((new frmDialog()))).ShowDialogNoneBlock(this);
ソースコード:
class NoneBlockingDialog
{
Form dialog;
Form Owner;
public NoneBlockingDialog(Form f)
{
this.dialog = f;
this.dialog.FormClosing += new FormClosingEventHandler(f_FormClosing);
}
void f_FormClosing(object sender, FormClosingEventArgs e)
{
if(! e.Cancel)
PUtils.SetNativeEnabled(this.Owner.Handle, true);
}
public void ShowDialogNoneBlock(Form owner)
{
this.Owner = owner;
PUtils.SetNativeEnabled(owner.Handle, false);
this.dialog.Show(owner);
}
}
partial class PUtils
{
const int GWL_STYLE = -16;
const int WS_DISABLED = 0x08000000;
[DllImport("user32.dll")]
static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")]
static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
static public void SetNativeEnabled(IntPtr hWnd, bool enabled)
{
SetWindowLong(hWnd, GWL_STYLE, GetWindowLong(hWnd, GWL_STYLE) & ~WS_DISABLED | (enabled ? 0 : WS_DISABLED));
}
}