我有一个 Windows 窗体应用程序 VS2010 C#,它显示一个带有“确定”按钮的消息框。
如何才能在用户离开时,消息框在超时(例如 5 秒)后自动关闭?
尝试以下方法:
AutoClosingMessageBox.Show("Text", "Caption", 1000);
其中
AutoClosingMessageBox
类实现如下:
public class AutoClosingMessageBox {
System.Threading.Timer _timeoutTimer;
string _caption;
AutoClosingMessageBox(string text, string caption, int timeout) {
_caption = caption;
_timeoutTimer = new System.Threading.Timer(OnTimerElapsed,
null, timeout, System.Threading.Timeout.Infinite);
using(_timeoutTimer)
MessageBox.Show(text, caption);
}
public static void Show(string text, string caption, int timeout) {
new AutoClosingMessageBox(text, caption, timeout);
}
void OnTimerElapsed(object state) {
IntPtr mbWnd = FindWindow("#32770", _caption); // lpClassName is #32770 for MessageBox
if(mbWnd != IntPtr.Zero)
SendMessage(mbWnd, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
_timeoutTimer.Dispose();
}
const int WM_CLOSE = 0x0010;
[System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true)]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto)]
static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);
}
更新: 如果您想在用户在超时之前选择某些内容时获取底层 MessageBox 的返回值,您可以使用此代码的以下版本:
var userResult = AutoClosingMessageBox.Show("Yes or No?", "Caption", 1000, MessageBoxButtons.YesNo);
if(userResult == System.Windows.Forms.DialogResult.Yes) {
// do something
}
...
public class AutoClosingMessageBox {
System.Threading.Timer _timeoutTimer;
string _caption;
DialogResult _result;
DialogResult _timerResult;
AutoClosingMessageBox(string text, string caption, int timeout, MessageBoxButtons buttons = MessageBoxButtons.OK, DialogResult timerResult = DialogResult.None) {
_caption = caption;
_timeoutTimer = new System.Threading.Timer(OnTimerElapsed,
null, timeout, System.Threading.Timeout.Infinite);
_timerResult = timerResult;
using(_timeoutTimer)
_result = MessageBox.Show(text, caption, buttons);
}
public static DialogResult Show(string text, string caption, int timeout, MessageBoxButtons buttons = MessageBoxButtons.OK, DialogResult timerResult = DialogResult.None) {
return new AutoClosingMessageBox(text, caption, timeout, buttons, timerResult)._result;
}
void OnTimerElapsed(object state) {
IntPtr mbWnd = FindWindow("#32770", _caption); // lpClassName is #32770 for MessageBox
if(mbWnd != IntPtr.Zero)
SendMessage(mbWnd, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
_timeoutTimer.Dispose();
_result = _timerResult;
}
const int WM_CLOSE = 0x0010;
[System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true)]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto)]
static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);
}
又一个更新
我用
YesNo
按钮检查了@Jack的案例,发现发送WM_CLOSE
消息的方法根本不起作用。Install-Package AutoClosingMessageBox
发行说明 (v1.0.0.2):
Show(IWin32Owner)
API 支持最流行的场景(在
#1)的上下文;AutoClosingMessageBox.Factory()
API 可提供对 MessageBox 显示的完全控制;发行说明 (v1.0.0.3):
适用于 WinForms 的解决方案:
var w = new Form() { Size = new Size(0, 0) };
Task.Delay(TimeSpan.FromSeconds(10))
.ContinueWith((t) => w.Close(), TaskScheduler.FromCurrentSynchronizationContext());
MessageBox.Show(w, message, caption);
基于关闭拥有消息框的表单也会关闭消息框的效果。
Windows 窗体控件要求必须在创建它们的同一线程上访问它们。假设上面的示例代码在 UI 线程或用户创建的线程上执行,使用
TaskScheduler.FromCurrentSynchronizationContext()
将确保这一点。如果代码在线程池(例如计时器回调)或任务池(例如使用 TaskFactory.StartNew
或 Task.Run
使用默认参数创建的任务上)的线程上执行,则该示例将无法正常工作。
如果您不介意稍微混淆您的参考文献,您可以包含
Microsoft.Visualbasic,
并使用这种非常简短的方式。
显示消息框
(new System.Threading.Thread(CloseIt)).Start();
MessageBox.Show("HI");
关闭功能:
public void CloseIt()
{
System.Threading.Thread.Sleep(2000);
Microsoft.VisualBasic.Interaction.AppActivate(
System.Diagnostics.Process.GetCurrentProcess().Id);
System.Windows.Forms.SendKeys.SendWait(" ");
}
现在去洗手吧!
你可以试试这个:
[DllImport("user32.dll", EntryPoint="FindWindow", SetLastError = true)]
static extern IntPtr FindWindowByCaption(IntPtr ZeroOnly, string lpWindowName);
[DllImport("user32.Dll")]
static extern int PostMessage(IntPtr hWnd, UInt32 msg, int wParam, int lParam);
private const UInt32 WM_CLOSE = 0x0010;
public void ShowAutoClosingMessageBox(string message, string caption)
{
var timer = new System.Timers.Timer(5000) { AutoReset = false };
timer.Elapsed += delegate
{
IntPtr hWnd = FindWindowByCaption(IntPtr.Zero, caption);
if (hWnd.ToInt32() != 0) PostMessage(hWnd, WM_CLOSE, 0, 0);
};
timer.Enabled = true;
MessageBox.Show(message, caption);
}
System.Windows.MessageBox.Show() 方法有一个重载,它将所有者 Window 作为第一个参数。如果我们创建一个不可见的所有者窗口,然后在指定时间后关闭它,它的子消息框也会关闭。
Window owner = CreateAutoCloseWindow(dialogTimeout);
MessageBoxResult result = MessageBox.Show(owner, ...
到目前为止一切顺利。但是,如果 UI 线程被消息框阻塞并且无法从工作线程访问 UI 控件,我们如何关闭窗口呢?答案是 - 通过向所有者窗口句柄发送 WM_CLOSE 窗口消息:
Window CreateAutoCloseWindow(TimeSpan timeout)
{
Window window = new Window()
{
WindowStyle = WindowStyle.None,
WindowState = System.Windows.WindowState.Maximized,
Background = System.Windows.Media.Brushes.Transparent,
AllowsTransparency = true,
ShowInTaskbar = false,
ShowActivated = true,
Topmost = true
};
window.Show();
IntPtr handle = new WindowInteropHelper(window).Handle;
Task.Delay((int)timeout.TotalMilliseconds).ContinueWith(
t => NativeMethods.SendMessage(handle, 0x10 /*WM_CLOSE*/, IntPtr.Zero, IntPtr.Zero));
return window;
}
这是 SendMessage Windows API 方法的导入:
static class NativeMethods
{
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);
}
RogerB对此答案有最巧妙的解决方案之一,他早在 04 年就做到了,而且现在仍然很成功。
基本上,您转到他的项目并下载 CS 文件。万一该链接失效了,我在这里有一个备份gist。将 CS 文件添加到您的项目中,或者如果您愿意,可以将代码复制/粘贴到某处。
然后,你所要做的就是切换
DialogResult result = MessageBox.Show("Text","Title", MessageBoxButtons.CHOICE)
到
DialogResult result = MessageBoxEx.Show("Text","Title", MessageBoxButtons.CHOICE, timer_ms)
你就可以走了。
您可以在
MessageBoxTimeout()
中使用 User32.dll
。
这是 Microsoft Windows 使用的一个未记录的函数,它完全可以满足您的需求,甚至更多。它还支持不同的语言。
C# 导入:
[DllImport("user32.dll", SetLastError = true)]
public static extern int MessageBoxTimeout(IntPtr hWnd, String lpText, String lpCaption, uint uType, Int16 wLanguageId, Int32 dwMilliseconds);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr GetForegroundWindow();
如何在C#中使用它:
uint uiFlags = /*MB_OK*/ 0x00000000 | /*MB_SETFOREGROUND*/ 0x00010000 | /*MB_SYSTEMMODAL*/ 0x00001000 | /*MB_ICONEXCLAMATION*/ 0x00000030;
NativeFunctions.MessageBoxTimeout(NativeFunctions.GetForegroundWindow(), $"Kitty", $"Hello", uiFlags, 0, 5000);
更聪明地工作,而不是更努力地工作。
我就是这样做的
var owner = new Form { TopMost = true };
Task.Delay(30000).ContinueWith(t => {
owner.Invoke(new Action(()=>
{
if (!owner.IsDisposed)
{
owner.Close();
}
}));
});
var dialogRes = MessageBox.Show(owner, msg, "Info", MessageBoxButtons.YesNo, MessageBoxIcon.Information);
DMitryG 的“获取底层 MessageBox
的返回值”的代码有一个错误,因此 timerResult 永远不会真正正确返回(
MessageBox.Show
调用在 OnTimerElapsed
完成后返回)。我的修复如下:
public class TimedMessageBox {
System.Threading.Timer _timeoutTimer;
string _caption;
DialogResult _result;
DialogResult _timerResult;
bool timedOut = false;
TimedMessageBox(string text, string caption, int timeout, MessageBoxButtons buttons = MessageBoxButtons.OK, DialogResult timerResult = DialogResult.None)
{
_caption = caption;
_timeoutTimer = new System.Threading.Timer(OnTimerElapsed,
null, timeout, System.Threading.Timeout.Infinite);
_timerResult = timerResult;
using(_timeoutTimer)
_result = MessageBox.Show(text, caption, buttons);
if (timedOut) _result = _timerResult;
}
public static DialogResult Show(string text, string caption, int timeout, MessageBoxButtons buttons = MessageBoxButtons.OK, DialogResult timerResult = DialogResult.None) {
return new TimedMessageBox(text, caption, timeout, buttons, timerResult)._result;
}
void OnTimerElapsed(object state) {
IntPtr mbWnd = FindWindow("#32770", _caption); // lpClassName is #32770 for MessageBox
if(mbWnd != IntPtr.Zero)
SendMessage(mbWnd, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
_timeoutTimer.Dispose();
timedOut = true;
}
const int WM_CLOSE = 0x0010;
[System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto)]
static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);
}
使用
EndDialog
而不是发送 WM_CLOSE
:
[DllImport("user32.dll")]
public static extern int EndDialog(IntPtr hDlg, IntPtr nResult);
Vb.net 库有一个使用交互类的简单解决方案:
void MsgPopup(string text, string title, int secs = 3)
{
dynamic intr = Microsoft.VisualBasic.Interaction.CreateObject("WScript.Shell");
intr.Popup(text, secs, title);
}
bool MsgPopupYesNo(string text, string title, int secs = 3)
{
dynamic intr = Microsoft.VisualBasic.Interaction.CreateObject("WScript.Shell");
int answer = intr.Popup(text, secs, title, (int)Microsoft.VisualBasic.Constants.vbYesNo + (int)Microsoft.VisualBasic.Constants.vbQuestion);
return (answer == 6);
}
使用计时器并在消息框出现时开始。 如果您的 MessageBox 只监听 OK 按钮(只有 1 种可能性),则使用 OnTick-Event 来模拟 ESC-Press
SendKeys.Send("{ESC}");
,然后停止计时器。
还有一个可用的 codeproject 项目此处,它提供您所需的功能。
我的用例略有不同,因为我需要允许用户在另一个任务运行时取消该任务。因此,我制作了上面使用的 Form 方法的异步版本:
public static async Task<DialogResult?> ShowDialogAsync(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, CancellationToken cancellationToken)
{
// Create a dummy form to parent the messagebox
var form = new Form();
// We'll need to dispose of the parent form to destroy the messagbeox
Action disposeForm = () =>
{
if (!form.IsDisposed)
{
// Async tasks can resume on a different thread to the one they started on
// so we might need to invoke this to prevent a cross-thread exception
if (form.InvokeRequired)
{
form.BeginInvoke(new Action(() => form.Dispose()));
}
else
{
form.Dispose();
}
}
};
try
{
// subscribe to the cancellation and close/dispose of the form if cancellation is required
using (var cancellationRegistration = cancellationToken.Register(() => disposeForm()))
{
var result = await Task.Run<DialogResult>(() => MessageBox.Show(form, text, caption, buttons, icon));
// If cancellation is requested we return null, otherwise we return the result of the dialog
if (cancellationToken.IsCancellationRequested)
{
return null;
}
return result;
}
}
finally
{
// we always want to close/dispose the form
disposeForm();
}
}
定时取消的用途:
var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(2)); // timeout 2 seconds
DialogResult? result = await ShowDialogAsync("some text", "some title", MessageBoxButtons.OK, MessageBoxIcon.Information);
if (result.HasValue)
{
if (result == MessageBoxButtons.OK)
{
// do something
}
}
else
{
// the dialog timed out
}
我自己的用法:
private async void button1_Click(object sender, EventArgs e)
{
// I've got two cancellation token sources: one for the messagebox, one for the other task
var cancellationTokenSource = new CancellationTokenSource();
var otherTaskCancellationTokenSource = new CancellationTokenSource();
// Show the dialog and also start the other task
var dialogTask = ShowDialogAsync("The other task is running now.", "Title", MessageBoxButtons.OKCancel, MessageBoxIcon.Information, cancellationTokenSource.Token);
var otherTask = OtherAsyncWork(otherTaskCancellationTokenSource.Token);
try
{
// wait until either of the tasks complete (i.e. a button is pressed, or OtherAsyncWork completes)
await Task.WhenAny(dialogTask, otherTask);
}
catch (OperationCanceledException)
{
// If otherTask got cancelled, we should get rid of the messagebox as it's not relevant anymore
cancellationTokenSource.Cancel();
}
var result = await dialogTask;
if (result.HasValue && result.Value == DialogResult.Cancel)
{
// The user selected cancel so we should cancel the other task
otherTaskCancellationTokenSource.Cancel();
}
try
{
// Wait for the other task to complete
await otherTask;
}
catch (OperationCanceledException)
{
MessageBox.Show("other task was cancelled.");
}
}
// Dummy task with 10-second delay
public async Task OtherAsyncWork(CancellationToken cancellationToken)
{
await Task.Delay(10000, cancellationToken);
MessageBox.Show("other task completed.");
}
为了与利用
owner
的 ShowDialog
论点的答案保持一致,这个助手对我有用。
public class EphemeralParent : Form
{
public EphemeralParent(TimeSpan timeout)
{
_ = Handle;
Task
.Delay(timeout)
.GetAwaiter()
.OnCompleted(()=>Dispose());
}
}
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
buttonShow.Click += (sender, e) =>
{
var result = MessageBox.Show(
owner: new EphemeralParent(TimeSpan.FromSeconds(2.5)),
"Hello",
"Auto Close",
MessageBoxButtons.YesNoCancel);
Text = result.ToString();
};
}
}
user32.dll 中有一个未记录的 API,名为 MessageBoxTimeout(),但它需要 Windows XP 或更高版本。