我希望我能足够清楚地解释这一点。我有我的主表单 (A),它使用 form.Show() 打开 1 个子表单 (B),使用 form.Show() 打开第二个子表单 (C)。现在我希望子窗体 B 使用 form.ShowDialog() 打开一个窗体 (D)。当我这样做时,它也会阻止表格 A 和表格 C。有没有办法打开一个模式对话框,只让它阻止打开它的表单?
11 回答
使用多个 GUI 线程是一件棘手的事情,如果这是您这样做的唯一动机,我建议您不要这样做。
更合适的方法是使用Show()
而不是ShowDialog()
,并禁用所有者表单,直到弹出表单返回。只有四个考虑:
使用
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()
,则需要将逻辑分成两部分。对话框关闭后应该运行的代码(包括重新启用所有者表单)应该由Closed
事件处理程序运行。当表单显示为对话框时,设置其
DialogResult
属性会自动关闭它。每当单击具有DialogResult
其他属性的按钮时,都会设置此属性None
。显示的表单Show
不会像这样自动关闭,因此我们必须在单击其中一个关闭按钮时显式关闭它。但是请注意,该DialogResult
属性仍由按钮正确设置。
实现这四件事,您的代码将变为:
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);
}
(显然,您实际上不会像上面那样构建代码 - 这只是显示行为的最短方式;在实际代码中,每个表单都有一个类,等等)
如果您在 A 和 C 的单独线程上运行 Form B,则 ShowDialog 调用只会阻塞该线程。显然,这当然不是一项微不足道的工作投资。
您可以让对话框完全不阻塞任何线程,只需在单独的线程上运行 Form D 的 ShowDialog 调用。这需要相同类型的工作,但要少得多,因为您的应用程序主线程只会运行一个表单。
我想总结可能的解决方案并添加一个新的替代方案(3a 和 3b)。但首先我想澄清一下我们在说什么:
我们有一个具有多种形式的应用程序。需要显示模态对话框,它只会阻止我们表单的某些子集,而不会阻止其他表单。模态对话框可能仅在一个子集(场景 A)或多个子集(场景 B)中显示。
现在总结可能的解决方案:
完全不要使用通过显示
ShowDialog()
的模态形式考虑一下您的应用程序的设计。你真的需要使用
ShowDialog()
方法吗?如果您不需要模态形式,这是最简单和最干净的方法。当然,这种解决方案并不总是合适的。有一些特性
ShowDialog()
给了我们。最值得注意的是它禁用了所有者(但不会灰显)并且用户无法与之交互。非常令人筋疲力尽的答案提供了P Daddy。模仿
ShowDialog()
行为可以模拟该方法的行为。我再次建议阅读P Daddy 的回答。
a) 通过 . 使用
Enabled
属性Form
组合并将形式显示为非模态Show()
。因此,禁用的表单将灰显。但它是完全托管的解决方案,不需要任何互操作。b)不喜欢父表单变灰?参考一些本地方法并关闭
WS_DISABLED
父表单上的位(再次 - 请参阅P Daddy的答案)。这两种解决方案要求您完全控制需要处理的所有对话框。您必须使用特殊构造来显示“部分阻塞对话框”,并且不能忘记它。你需要调整你的逻辑,因为
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 是在主线程上运行的表单。我有一个帮助对话框,我想以无模式对话框的形式运行。这很容易实现,甚至可以确保我只运行一个帮助对话框实例。不幸的是,我使用的任何模态对话框也会导致帮助对话框失去焦点——当其中一些模态对话框正在运行时,帮助对话框将是最有用的。
使用这里和其他地方提到的想法,我设法克服了这个错误。
我在主 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();
}
}
我还需要初始化添加到构造函数中的线程,以确保在第一次运行此代码时我没有引用空对象。
public MainWindow()
{
...
helpThread = new Thread(new ThreadStart(startHelpThread));
helpThread.SetApartmentState(ApartmentState.STA);
...
}
这确保线程在任何给定时间只有一个实例。线程本身运行对话框,并在对话框关闭后停止。由于它在单独的线程上运行,因此从主 UI 中创建模式对话框不会导致帮助对话框挂起。我确实需要添加
helpDialog.Abort();
到我的主 UI 的表单关闭事件,以确保在应用程序终止时关闭帮助对话框。
我现在有一个无模式的帮助对话框,它不受从我的主 UI 中生成的任何模式对话框的影响,这正是我想要的。这是安全的,因为主 UI 和帮助对话框之间不需要通信。
这是我在 WPF 中使用的帮助程序,以根据对这个问题的一些答案来防止对话框阻塞非对话框窗口:
public static class WindowHelper
{
public static bool? ShowDialogNonBlocking(this Window window)
{
var frame = new DispatcherFrame();
void closeHandler(object sender, EventArgs args)
{
frame.Continue = false;
}
try
{
window.Owner.SetNativeEnabled(false);
window.Closed += closeHandler;
window.Show();
Dispatcher.PushFrame(frame);
}
finally
{
window.Closed -= closeHandler;
window.Owner.SetNativeEnabled(true);
}
return window.DialogResult;
}
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 void SetNativeEnabled(this Window window, bool enabled)
{
var handle = new WindowInteropHelper(window).Handle;
SetWindowLong(handle, GWL_STYLE, GetWindowLong(handle, GWL_STYLE) &
~WS_DISABLED | (enabled ? 0 : WS_DISABLED));
}
}
用法:
if(true == window.ShowDialogNonBlocking())
{
// Dialog result has correct value
}
也许子窗口(详见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));
}
}
我来到这个线程是为了做类似的事情:有多个独立的父窗体,在同一个进程中运行,每个都有自己的模式对话框。我打算用一个类比来证明它是可能的,那就是多个 Word 文档,每个文档都有自己的查找和替换窗口。
我确信我以前做过这个,但在尝试过之后,你知道吗,即使是 Microsoft Word 也无法做到这一点。它的行为与 OP 描述的完全一样:一旦打开模式对话框(如使用 Ctrl+H 查找和替换),所有 Word 文档都会被阻止并且无法与之交互。更糟糕的是,仅单击原始父文档会导致模式对话框闪烁。他们中的其他人只是没有回应,也没有暗示他们为什么被阻止或需要关闭什么。
这确实可能是一个非常令人困惑的用户体验。令我惊讶的是,微软自己的旗舰办公软件就是这种情况(而且我以前没有注意到)。然而,它也帮助我接受了我自己的应用程序的行为方式。
进一步的确认来自.NET 文档本身,该文档指出“ShowDialog
显示窗口,禁用应用程序中的所有其他窗口,并且仅在窗口关闭时返回”。
我意识到这在技术上不是一个“解决方案”。那些真正觉得他们的应用程序需要支持完全独立的父表单的人可以尝试@PDaddy的解决方法,这似乎非常彻底。
然而,希望这能让那些(像我一样)只是想认真对待他们的 UI 并认为他们做错了什么的人安心。你没疯。显然,这就是 Windows 的工作方式。