离开屏幕保护程序或锁定计算机后,程序卡住了。

10
我们的程序在计算机锁定或屏幕保护弹出(但不是Ctrl + Alt + Delete)时工作正常。一旦计算机解锁/屏幕保护关闭,应用程序停止绘制除标题栏以外的所有内容,并停止响应输入 - 它显示一个几乎全白的窗口,无法移动或关闭。

应用程序冻结示例

(应用程序冻结示例-山是我的桌面背景)

如果让它保持静止约5〜10分钟,它就会重新运行,并且在之后(即使在锁定计算机/屏幕保护弹出后)不会再挂起,直到重新启动应用程序。
这很难调试,因为它只发生在从Visual Studio启动程序时,而不是手动打开.exe时。 只有在显示启动画面画面时才会发生这种情况 - 如果我删除显示启动画面画面的代码,它就不会发生。但我们需要启动画面画面。 我已经尝试了此页面上的每个建议,唯一不会出现问题的是使用Microsoft.VisualBasic.WindowsFormsApplicationBase,但这会导致所有种类其他问题。
关于这个问题的信息在互联网上似乎很少 - 有人遇到过类似的问题吗?
以下是相关代码:
//Multiple programs use this login form, all have the same issue
public partial class LoginForm<TMainForm>
    where TMainForm : Form, new()
{
    private readonly Action _showLoadingForm;

    public LoginForm(Action showLoadingForm)
    {
        ...
        _showLoadingForm = showLoadingForm;
    }

    private void btnLogin_Click(object sender, EventArgs e)
    {
        ...
        this.Hide();
        ShowLoadingForm(); //Problem goes away when commenting-out this line
        new TMainForm().ShowDialog();
        this.Close();
    }

    private void ShowLoadingForm()
    {
        Thread loadingFormThread = new Thread(o => _showLoadingForm());
        loadingFormThread.IsBackground = true;
        loadingFormThread.SetApartmentState(ApartmentState.STA);
        loadingFormThread.Start();
    }
}

以下是一个程序中使用的_showLoadingForm动作的示例:

public static bool _showSplash = true;
public static void ShowSplashScreen()
{
    //Ick, DoEvents!  But we were having problems with CloseSplashScreen being called
    //before ShowSplashScreen - this hack was found at
    //https://dev59.com/NHVD5IYBdhLWcg3wNY5Z/48946#48946
    using(SplashForm splashForm = new SplashForm())
    {
        splashForm.Show();
        while(_showSplash)
            Application.DoEvents();
        splashForm.Close();
    }
}

//Called in MainForm_Load()
public static void CloseSplashScreen()
{
    _showSplash = false;
}

1
当程序被锁定时,你能否附加调试器? - Albin Sunnanbo
1
仔细查看它挂在哪里。它真的在 MainForm.ShowDialog 内部吗?还是在调用堆栈更上层有更具体的东西。如果它确实卡在 ShowDialog 内部,那么这意味着主 UI 线程上不再传递消息。这非常奇怪,可能表明 .NET 中存在一个晦涩的 bug,只有在显示闪屏时才会显现出来。这是一个相当奇怪的问题。 - Brian Gideon
1
顺便说一下,调用 Application.DoEvents 的 while 循环应该是不停地旋转。它消耗了很多 CPU 时间吗? - Brian Gideon
1
"_showSplash" 应该是 volatile 的,顺便说一下。 - Remus Rusanu
1
如果你看不到它,那就没有意义。这种代码经常会与SystemEvents类发生问题。你可能会让它混淆,从而在错误的线程上触发事件。 - Hans Passant
显示剩余11条评论
10个回答

3

启动画面问题

使用DoEvents是不可取的,也不一定能达到你想要的效果。DoEvents告诉CLR去处理窗口消息循环(用于启动画面),但并不一定给其他线程提供任何处理时间。Thread.Sleep()会给其他线程处理的机会,但并不一定允许你的启动画面的窗口消息循环继续发送消息。因此,如果必须使用循环,你确实需要两者都有,但是过一会儿我会建议完全摆脱这个循环。除了循环问题之外,我没有看到任何明确的方法来清理启动画面线程。你需要在某个地方执行Thread.Join()Thread.Abort()

我喜欢使用ManualResetEvent来同步启动画面窗体和调用线程。这样,只有当启动画面显示出来后,ShowSplash()方法才会返回。在此之后,我们显然可以关闭它,因为我们知道它已经完成了显示。

以下是一个带有几个好例子的线程:.NET Multi-threaded Splash Screens in C#

以下是我修改我最喜欢的示例(由@AdamNosfinger发布)以包括ManualResetEvent来将ShowSplash方法与启动画面线程同步的方式:

public partial class FormSplash : Form
{
    private static Thread _splashThread;
    private static FormSplash _splashForm;
    // This is used to make sure you can't call SplashScreenClose before the SplashScreenOpen has finished showing the splash initially.
    static ManualResetEvent SplashScreenLoaded;

    public FormSplash()
    {
        InitializeComponent();

        // Signal out ManualResetEvent so we know the Splash form is good to go.
        SplashScreenLoaded.Set();
    }

    /// <summary>
    /// Show the Splash Screen (Loading...)
    /// </summary>
    public static void ShowSplash()
    {
        if (_splashThread == null)
        {
            // Setup our manual reset event to syncronize the splash screen thread and our main application thread.
            SplashScreenLoaded = new ManualResetEvent(false);

            // show the form in a new thread
            _splashThread = new Thread(new ThreadStart(DoShowSplash));
            _splashThread.IsBackground = true;
            _splashThread.Start();

            // Wait for the splash screen thread to let us know its ok for the app to keep going. 
            // This next line will not return until the SplashScreen is loaded.
            SplashScreenLoaded.WaitOne();
            SplashScreenLoaded.Close();
            SplashScreenLoaded = null;
        }
    }

    // called by the thread
    private static void DoShowSplash()
    {
        if (_splashForm == null)
            _splashForm = new FormSplash();

        // create a new message pump on this thread (started from ShowSplash)
        Application.Run(_splashForm);
    }

    /// <summary>
    /// Close the splash (Loading...) screen
    /// </summary>
    public static void CloseSplash()
    {
        // need to call on the thread that launched this splash
        if (_splashForm.InvokeRequired)
            _splashForm.Invoke(new MethodInvoker(CloseSplash));

        else
            Application.ExitThread();
    }
}

主窗体问题

看起来你是使用ShowDialog从登录窗口启动你的主窗体,然后关闭登录窗口。如果是这样的话,这不太好。ShowDialog是用于应用程序的子窗口,并且需要一个所有者窗口,如果你没有在方法参数中指定所有者窗体,则当前活动窗口将被假定为所有者。请参阅MSDN

因此,你的主窗体假定登录窗体是其父窗体,但是你在显示主窗体后不久关闭了登录窗体。所以我不确定此时应用程序处于什么状态。如果需要的话,你应该考虑使用标准的Form.Show()方法,然后简单地调整窗体属性,使其看起来像对话框(例如BorderStyle、MaximizeBox、MinimizeBox、ControlBox、TopMost)。

重要编辑:好吧,我是人,我搞错了,忘记了ShowDialog是一个阻塞方法。虽然这可以消除所有者句柄问题,但我仍然建议不要在主应用程序窗体中使用ShowDialog,除非你能提供一个重大的理由,而这个理由与外观或线程相关(因为这些应该使用其他技术来解决)。尽管我有点失误,但建议仍然是正确的。

可能存在的绘图问题

你没有指定你在应用程序中使用哪些控件,也没有说明是否在进行任何自定义绘制。但需要记住,当你锁定计算机时,某些窗口句柄将被强制关闭。例如,如果你正在自定义绘制控件并缓存字体、画刷或其他GDI资源,则需要在代码中添加一些try { ... } catch { ... }块,在绘制过程中引发异常时处理并重新构建缓存的GDI资源。我之前遇到过这种情况,当时我正在自定义绘制列表框并缓存一些GDI对象。如果你的应用程序中有任何自定义绘图代码,包括闪屏界面,请仔细检查所有GDI对象是否被妥善释放/清理。


请重新阅读问题-我不仅说过我已经尝试了其他显示闪屏窗体的方法,而且我还明确地链接到了同一个主题!然而,这些方法中没有一个解决问题。这不是一个自定义绘画问题,因为程序没有被任何用户代码卡住(也在上面提到)。而且你似乎把Form.ShowDialog()和Form.Show()混淆了——ShowDialog()不会返回直到子窗体退出,所以this.Close()直到程序准备退出时才会被调用。 - BlueRaja - Danny Pflughoeft
你在 ShowDialog 是阻塞的这一点上是正确的,我的错 - 我会进行一些小的更正。但是为什么要使用 ShowDialog 并将登录窗体保留在后面呢?我确实阅读了您提供的参考帖子,您会注意到我修改了该问题中我最喜欢的答案,以包括一个阻塞的 ShowSplash 函数,因为您说在显示之前调用关闭时遇到了问题。我正在添加更多代码来修复可能存在的启动顺序问题。 - BenSwayne

1
几年后(没有代码在我面前),我会为任何遇到这个问题的人添加一个答案。
问题的原因正如Hans Passant所猜测的那样。问题在于,由于.NET框架中存在一些非常晦涩无比且微不足道的错误,InvokeRequired有时会返回false,而实际上应该返回true,导致本应在GUI线程上运行的代码在后台运行(由于另一些晦涩无比且微不足道的错误,这就导致了我看到的行为)
解决方法是不要依赖于InvokeRequired,使用类似于以下的hack方法:
void Main()
{
    Thread.Current.Name = "GuiThread";
    ...
}

bool IsGuiThread()
{
    return Thread.Current.Name == "GuiThread";
}

//Later, call IsGuiThread() to determine if GUI code is being run on GUI thread

这个解决方案以及对问题原因的极其深入的分析可以在这里找到。


1

在以上代码片段中添加几行代码后,我成功编译出了一个可用的程序。但是,我无法重现问题(Windows 7 Starter)。我尝试锁定计算机并启动屏幕保护程序,这些操作都是在闪屏界面激活时进行的,但无论何时主窗口都保持响应。我认为这里一定还有其他问题,可能是在主窗口初始化期间发生的。

以下是代码,也许它能帮助其他人找出问题所在。

using System;
using System.Threading;
using System.Windows.Forms; 

public class MainForm : Form
{
  //Here is an example of one of the _showLoadingForm actions used in one of the programs:
  public static bool _showSplash = true;
  public static void ShowSplashScreen()
  {
    //Ick, DoEvents!  But we were having problems with CloseSplashScreen being called
    //before ShowSplashScreen - this hack was found at
    //https://dev59.com/NHVD5IYBdhLWcg3wNY5Z#48946
    using(SplashForm splashForm = new SplashForm())
    {
      splashForm.Show();
      while(_showSplash)
        Application.DoEvents();
      splashForm.Close();
    }
  }

  //Called in MainForm_Load()
  public static void CloseSplashScreen()
  {
    _showSplash = false;
  }

  public MainForm() 
  { 
    Text = "MainForm"; 
    Load += delegate(object sender, EventArgs e) 
    {
      Thread.Sleep(3000);
      CloseSplashScreen(); 
    };
  }
}

//Multiple programs use this login form, all have the same issue
public class LoginForm<TMainForm> : Form where TMainForm : Form, new()
{
  private readonly Action _showLoadingForm;

  public LoginForm(Action showLoadingForm)
  {
    Text = "LoginForm";
    Button btnLogin = new Button();
    btnLogin.Text = "Login";
    btnLogin.Click += btnLogin_Click;
    Controls.Add(btnLogin);
    //...
    _showLoadingForm = showLoadingForm;
  }

  private void btnLogin_Click(object sender, EventArgs e)
  {
    //...
    this.Hide();
    ShowLoadingForm(); //Problem goes away when commenting-out this line
    new TMainForm().ShowDialog();
    this.Close();
  }

  private void ShowLoadingForm()
  {
    Thread loadingFormThread = new Thread(o => _showLoadingForm());
    loadingFormThread.IsBackground = true;
    loadingFormThread.SetApartmentState(ApartmentState.STA);
    loadingFormThread.Start();
  }
}

public class SplashForm : Form
{
  public SplashForm() 
  { 
    Text = "SplashForm"; 
  }
}

public class Program
{
  public static void Main()
  {
    var loginForm = new LoginForm<MainForm>(MainForm.ShowSplashScreen);
    loginForm.Visible = true;
    Application.Run(loginForm);
  }
}

0

由于没有可用的示例,

你能否尝试删除 Application.DoEvents(); 并插入一个 thread.sleep?

Application.DoEvents(); 可以说是非常危险的。


正如我所说,我尝试了this page上的所有其他建议。创建启动屏幕的任何方法都不起作用,因此不是由于“DoEvents()”。 - BlueRaja - Danny Pflughoeft

0
从我快速扫描您的代码来看,您的问题的关键可能在于使用以下内容。
Application.Run(_splashForm);

理想情况下,您应该在线程内使用它,但也许它可以与您的DoEvents一起使用。如果您正在这样做,而我只是错过了它,那么很抱歉...


0
在我们的应用程序中,我们遇到了与启动画面类似的问题。我们想要一个带有动画GIF的启动画面(别怪我,这是管理决定)。只有当splashScreen有自己的消息循环时,它才能正常工作。因为我认为DoEvents是解决您问题的关键,所以我向您展示了我们是如何解决它的。希望这能帮助您解决问题!
我们将以以下方式显示启动画面:
// AnimatedClockSplashScreen is a special form from us, it can be any other!
// Our form is set to be TopMost
splashScreen = new AnimatedClockSplashScreen(); 
Task.Factory.StartNew(() => Application.Run(splashScreen));

启动画面是一个简单的容器,包含了时钟的动画gif。它没有任何循环,因此不会占用任何时间。

当需要关闭启动画面时,我们会这样做:

if (splashScreen != null)
{
    if (splashScreen.IsHandleCreated)
    {
        try
        {
            splashScreen.Invoke(new MethodInvoker(() => splashScreen.Close()));
        }
        catch (InvalidOperationException)
        {
        }
    }
    splashScreen.Dispose();
    splashScreen = null;
}

0

删除此行,您不需要它,因为默认情况下是多线程应用程序。采用默认设置。

loadingFormThread.SetApartmentState(ApartmentState.STA);

更改以下内容:

using(SplashForm splashForm = new SplashForm())
{
    splashForm.Show();
    while(_showSplash)
        Application.DoEvents();
    splashForm.Close();
}

to:

SplashForm splashForm = new SplashForm())
splashForm.Show();

将此更改为:

public static void CloseSplashScreen()
{
    _showSplash = false;
}

转换为:

public static void CloseSplashScreen()
{
    splashForm.Close();
}

0

你尝试过在线程中使用 WaitHandle 来显示窗体吗?

类似这样:

EventWaitHandle _waitHandle = new AutoResetEvent(false);
public static void ShowSplashScreen()
{
    using(SplashForm splashForm = new SplashForm())
    {
        splashForm.Show();
        _waitHandle.WaitOne();
        splashForm.Close();
    }
}

//Called in MainForm_Load()
public static void CloseSplashScreen()
{
    _waitHandle.Set();
}

0
这是一个尝试:当我们处于空闲状态时,我们也会要求线程进入睡眠状态。我不确定这是否有帮助,但值得一试:
    while(_showSplash) {
        System.Threading.Thread.Sleep(500);
        Application.DoEvents();
    }

0

我认为你的问题是因为你使用了Form.ShowDialog而不是Application.Run。ShowDialog运行在主消息循环之上的受限制的消息循环,并忽略一些窗口消息。

像这样的代码应该可以解决问题:

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

        Application.Run( new MainForm() );
    }
}


public partial class MainForm: Form
{
    FormSplash dlg = null;

    void ShowSplashScreen()
    {
        var t = new Thread( () =>
            {
                using ( dlg = new FormSplash() ) dlg.ShowDialog();
            }
        );

        t.SetApartmentState( ApartmentState.STA );
        t.IsBackground = true;
        t.Start();
    }

    void CloseSplashScreen()
    {
        dlg.Invoke( ( MethodInvoker ) ( () => dlg.Close() ) );
    }

    public MainForm()
    {
        ShowSplashScreen();

        InitializeComponent();

        Thread.Sleep( 3000 ); // simulate big form

        CloseSplashScreen();
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接