【问题标题】:Open Winforms screen after every one minute if the form is closed如果表单关闭,每隔一分钟打开一次 Winforms 屏幕
【发布时间】:2020-10-01 23:06:10
【问题描述】:

只有在有事件时才打开表单,如果没有事件,则它不应该显示在屏幕上。所以基本上我想用一个计时器来做到这一点。一个 exe 将持续运行,并且每分钟它都会检查数据库以查看是否有数据,如果有数据,是否会显示在屏幕上,并且只会通过用户交互手动关闭。一分钟后,它会再次检查并显示数据库中是否存在数据。

我在 Program.cs 文件中使用 system.threading.Timer 每分钟打开一个窗口。下面是代码

timer = new System.Threading.Timer((s) => { 
  EL.CustomMessageBox l = new EL.CustomMessageBox(); 
  l.ShowDialog();                
}, null, TimeSpan.Zero, 60000);

一段时间后,我看到这个 exe 仍在任务管理器中运行,但即使数据库中有数据,它也会停止显示在屏幕上。任何帮助表示赞赏。

【问题讨论】:

  • 在创建新的对话框对象之前放置一条控制台日志消息,以查看计时器是否仍在触发 - 这至少会告诉您问题是与对话框相关还是计时器正在消亡。跨度>
  • 您不能在非 UI 线程上创建、访问或更新任何 UI 元素。
  • 除了只在 UI 线程上运行之外,还可以考虑最小化和恢复表单,而不是让模态对话框弹出然后消失。
  • @Enigmativity:这完全是真的。您在其中创建 UI 元素的任何线程都是 UI 线程。
  • 您需要一个消息泵。不是创建线程那么简单。

标签: c# .net winforms


【解决方案1】:

System.Threading.Timer 在线程池线程上运行其回调。你不应该在 UI 工作中使用线程池线程,因为:

  1. 它们不运行消息分发循环。
  2. 您无法控制线程何时回收。 UI 窗口具有线程关联性,如果它们的线程退出,所有关联的窗口都会立即失效(您甚至不会收到 WM_DESTROY 消息)。

主线程上的普通 Application.Run 循环,带有隐藏的主窗口和 UI 计时器会更好地为您服务。

【讨论】:

  • 您好 Ben 感谢您的回复。我遵循了这种方法,看起来它正在工作。
【解决方案2】:

我会将自己的自定义 ApplicationContext 传递给 Application.Run() 中的 program.cs

这将允许您在满足您的条件之前没有接口。该应用程序还将继续运行(即使您关闭了表单),直到您明确调用 Application.Exit()

您可以在班级级别保留对表单的引用。这将帮助您决定是需要使用现有的还是创建一个新的。

请注意,我使用的是System.Windows.Forms.Timer,而不是线程计时器。

类似...

static class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new MyContext());
    }
}

public class MyContext : ApplicationContext
{

    private EL.CustomMessageBox l = null;
    private System.Windows.Forms.Timer timer;

    public MyContext()
    {
        timer = new System.Windows.Forms.Timer();
        timer.Interval = (int)TimeSpan.FromMinutes(1).TotalMilliseconds;
        timer.Tick += Timer_Tick;
        timer.Start();
    }

    private void Timer_Tick(object sender, EventArgs e)
    {
        bool result = true; // hit the database and get an answer
        if (result)
        {
            if (l == null || l.IsDisposed)
            {
                // no form has been created yet, or the previous one was closed
                // create a new instance
                l = new EL.CustomMessageBox();
                l.Show();
            }
            else
            {
                // if we get in here, then the previous form is still being displayed
                // if your form can be minimized, you might need to restore it
                // if (l.WindowState == FormWindowState.Minimized)
                // {
                //     restore the window in here?
                // }
            }

            // update the form "l" with some data?
            l.xxx = yyy;
        }
    }

}

【讨论】:

    【解决方案3】:

    我不禁认为,其他答案虽然在技术上是正确的,但实际上并不能解决问题,因为如果您不了解 Windows 的工作原理,它们可能没有意义。 Idle_Mind 最接近我所做的,但如果表单设计师熟悉,我会选择一个基本上只是使用它的解决方案 - 因此,我将展示我将做些什么来解决你面临的任务:

    • 拥有一个带有一个表单的应用程序(或者使这个表单在另一个应用程序中成为一个自治的表单,但现在可能将其作为一个专用应用程序来简化) - 创建一个新的 Windows 窗体项目

    • 有一个间隔为 60000 且 Enabled = true 的计时器(Windows 窗体计时器,工具箱外,不是 System.Threading 计时器)

    • 在您的表单上有一个计时器 Tick 事件处理程序(双击表单设计器下托盘中的计时器以附加事件处理程序)查询数据库并查找是否有任何消息

    • 如果有新消息,将它们添加到列表框或其他内容中,并调用this.Show() 显示表单

    • 将事件处理程序附加到 FormClosing 事件,这样当用户单击 X 时,表单会隐藏而不是关闭:

    private void MyForm_FormClosing(object sender, FormClosingEventArgs e)
    {
        if (e.CloseReason == CloseReason.UserClosing) 
        {
            e.Cancel = true;
            Hide();
        }
    }
    
    • 可能让 FormClosing 事件清除消息列表框。这样,如果表单打开并且用户正在吃午饭,消息将不断累积,然后他们可以通过关闭表单来阅读和清除它们。在已经可见的表单上调用 Show 不会执行任何操作,因此如果有更多消息进入并且表单已经可见,消息只会累积到列表框中

    良好的快速经验法则;永远不要在 Windows 窗体应用程序中使用 System.Threading Timer。改用表单设计器工具箱中的计时器。如果您正在编写服务或控制台应用程序等,请仅使用线程计时器。出于稳定性原因,Windows 控件绝对必须由最初创建控件的线程访问。 Windows 窗体计时器意识到这一点,它的 Tick 事件可以安全地访问窗体应用程序中的控件(窗体是一个控件,显示它需要访问它)

    【讨论】:

    • 感谢您的详细解释。它有助于。我已经实现了它,目前正在测试它,它看起来很棒。将在周末运行它,看看它是否继续运行。
    • 我唯一的建议是确保数据库调用不会因未处理的异常而失败 - 在某些时候会发生一些虚假超时或某些事情,例如connection.Open() 将引发错误,因此如果您想确保应用程序继续运行,请确保特别处理这一点。此外,如果显示的消息很大,请设置一些限制总数的东西(例如删除所有早于 X 天的消息),这样您就不会得到一个不断消耗内存的应用程序,因为它会添加数千条消息到列表框
    • 谢谢你一定会记住这一点。
    【解决方案4】:

    您应该调用Invoke 在拥有控件底层窗口句柄的线程上执行您的委托。

    这样的事情应该可以工作:

    timer = new System.Threading.Timer((s) => { 
      EL.CustomMessageBox l = new EL.CustomMessageBox(); 
      l.Invoke((Action) () => 
      {
        l.ShowDialog();
      });               
    }, null, TimeSpan.Zero, 60000);
    

    或者更好的是,使用这个扩展方法:

    public static void InvokeIfRequired(this Control c, MethodInvoker action)
    {
        if (c.InvokeRequired)
        {
            c.Invoke(action);
        }
        else
        {
            action();
        }
    }
    

    然后这样称呼它:

    l.InvokeIfRequired(() => { l.ShowDialog(); });
    

    更多信息请访问:https://docs.microsoft.com/en-us/dotnet/desktop/winforms/controls/how-to-make-thread-safe-calls-to-windows-forms-controls?view=netframeworkdesktop-4.8

    【讨论】:

    • 他只是创建了窗口,所以“拥有控件底层窗口句柄的线程”就是当前线程,调用Invoke不会有任何区别。
    • 他在哪里说“我刚刚创建了线程”,也许它是一个winforms应用程序并且有一个消息循环。 OP 没有共享足够的代码示例来推断这一点。他没有说他的项目类型或他所处的同步环境。
    • 不是“刚刚创建线程”。他只是创建了窗口new EL.CustomMessageBox 发生在前一行。
    • 如果我们在 WinForms 应用程序和单线程单元中。我们可以在运行时允许我们创建尽可能多的窗口。所以我们不知道他是刚刚创建了他的第一个窗口,还是他只是通过调用ShowDialog 打开了另一个表单。我们不应该在没有看到完整代码的情况下做出假设。
    • 是不是第一个都没关系。哪个线程拥有您正在调用 Invoke() 的对象 l 很重要。这显然是当前线程,因为l 的构造就在我们给出的代码中。
    猜你喜欢
    • 2017-06-11
    • 1970-01-01
    • 2012-06-30
    • 2010-12-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多