TopMost在不同线程中连续运行并由用户代码关闭的窗体上使用时不起作用

本文关键字:窗体 不起作用 代码 用户 线程 连续 运行 TopMost | 更新日期: 2023-09-27 18:28:15

我有以下示例代码

   [STAThread]
    static void Main(string[] args)
    {
        Thread thread = new Thread(() =>
        {
            using (var mww = new Form1())
            {
                Application.Run(mww);
            }
        });
        thread.Start();
        thread.Join();
        thread = new Thread(() =>
        {
            using (var mww = new Form1())
            {
                Application.Run(mww);
            }
        });
        thread.Start();
        thread.Join();
        thread = new Thread(() =>
        {
            using (var mww = new Form1())
            {
                Application.Run(mww);
            }
        });
        thread.Start();
        thread.Join();
    }

显示Form1定义为:

public partial class Form1 : Form
{
    private readonly Timer _myTimer = new Timer();
    public Form1()
    {
        InitializeComponent();
        _myTimer.Interval = 10000;
        _myTimer.Tick += TOnTick;
        TopMost = true;
    }
    private void TOnTick(object sender, EventArgs eventArgs)
    {
        _myTimer.Stop();
        Close();
    }
    private void Form1_Load(object sender, EventArgs e)
    {
        _myTimer.Start();
    }
}
partial class Form1
{
    /// <summary>
    /// Required designer variable.
    /// </summary>
    private System.ComponentModel.IContainer components = null;
    /// <summary>
    /// Clean up any resources being used.
    /// </summary>
    /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
    protected override void Dispose(bool disposing)
    {
        if (_myTimer != null)
        {
            _myTimer.Dispose();
            _myTimer = null;
        }
        if (disposing && (components != null))
        {
            components.Dispose();
        }
        base.Dispose(disposing);
    }
    #region Windows Form Designer generated code
    /// <summary>
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// </summary>
    private void InitializeComponent()
    {
        this.SuspendLayout();
        // 
        // Form1
        // 
        this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
        this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
        this.ClientSize = new System.Drawing.Size(284, 262);
        this.Name = "Form1";
        this.Text = "Form1";
        this.Load += new System.EventHandler(this.Form1_Load);
        this.ResumeLayout(false);
    }
    #endregion
}

这个代码是从我的生产代码中提取的非常简单的代码,所以请不要告诉我它是无用的。

如果我把所有的Application.Run都放在一个线程中,那么一切都会正常工作,所有三个表单都会是TopMost。如果我按原样运行代码,则第一个表单显示为TopMost,而第二个和第三个表单则不显示。

但是:如果我通过显示表单上的红色X关闭按钮关闭表单(因此计时器的tick事件处理程序中的关闭方法没有被调用),下一个表单将显示为TopMost。在我看来,表单上的X关闭按钮和代码中调用的关闭方法之间一定有一些区别。但我不知道有什么区别,也不知道如何达到想要的行为:按计时器关闭,所有窗口都在最上面。

感谢的帮助

TopMost在不同线程中连续运行并由用户代码关闭的窗体上使用时不起作用

    TopMost = true;

这并不像你想象的那样。Windows有一个"温和"answers"粗糙"的版本,可以使窗口成为顶部。Winforms实现了它的温和版本,这是最不可能让用户感到不安的版本。一般来说,当一个程序把一个窗口塞进用户的脸上时,他们确实会非常沮丧。

Windows对此有对策,确保程序不能这样做,除非很可能用户不会对此感到不安。这是一种启发式方法,基于进程拥有的窗口何时获得用户输入。就像您单击"关闭"按钮一样。用户正在积极使用该窗口的信号。这不仅仅需要点击关闭按钮,例如,你可以点击窗口或按下光标键,你会看到你的程序成功地保持了前台的爱。

程序中的主要问题是当第一个窗口关闭时会发生什么。您的流程中没有可以获得焦点的窗口。因此,Windows窗口管理器被迫找到另一个窗口,该窗口将由一个完全不同的程序拥有。就像Visual Studio。现在启发式开始发挥作用,如果Windows不相信你应得的,你就无法获得前景。在不同的线程中显示另一个窗口不足以说服它。

好吧,你做错了,从你创建的程序暂时没有窗口的条件开始。你可以调用TopMost的原始版本,这很容易做到:

protected override CreateParams CreateParams {
    get {
        var cp = base.CreateParams;
        cp.ExStyle |= 8;  // Turn on WS_EX_TOPMOST
        return cp;
    }
}

你会发现你的窗口现在确实是最顶端的。粗糙的类型,由完全依赖于拥有最顶层窗口的程序使用,如osk.exe(屏幕键盘程序)。出于显而易见的原因,这种方式也会让用户感到不安。

我确实需要对你的方法发出强烈的警告,在不同的线程上显示UI在其他方面非常麻烦。真正糟糕的一种,你的程序在没有明显原因的情况下随机挂起。SystemEvents类是一个主要的麻烦制造者,它试图在UI线程上激发事件,但当程序有多个事件时,当然不能可靠地做到这一点。在许多Winforms程序中,结果往往很糟糕,工具箱中有很多控件在错误的线程上触发UserPreferenceChanged事件时无法处理该事件。诊断挂起所需的调试会话类型如下所示。这是为了吓唬你,不要这么做。

每个表单都有一个OnClosed&OnClosing事件。你所做的是明确关闭表单。

如果要在单击红色X时停止计时器,则需要使用OnClosedOnClosing

private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
    _myTimer.Stop();
    _myTimer.Dispose();
}
private void TOnTick(object sender, EventArgs eventArgs)
{
    //_myTimer.Stop();
    //_myTimer.Dispose();  No need to call, as it will be redundant
    Close();   // Will call FormClosed
}

你现在实际要做的是在计时器的每一次滴答声中停止计时器,处理它,然后关闭表单。


编辑:

实际上,我现在可以重现你的问题,如果我试图在窗体1上移动其他窗口,那么下一个窗口不是最上面的。

当时我做了一些研究,陈完美地解释了为什么它不起作用。

"如果两个程序都这样做了怎么办?"这个问题也有助于评估一个功能或设计请求。将其与"想象一下,如果可能"导致了令人印象深刻的一拳两拳。以下是一些示例:

"我如何创建一个从未被任何其他窗口覆盖的窗口,甚至连其他最顶层的窗户都没有?"

想象一下如果这是可能的,想象一下如果两个程序做到了这一点。程序A创建了一个"超级顶层"的窗口,程序也是如此B.现在用户拖动两个窗口,使它们重叠。什么发生了什么?你给自己制造了一种逻辑上的不可能。其中之一两扇窗户必须在另一扇上面,与想象的相矛盾"超级顶端"功能。

我在这个答案中发现了这一点,上面写着:

除非其他程序正在创建最顶层的窗口,否则Form.TopMost将起作用。

没有办法创建一个不被另一个进程的最顶层新窗口覆盖的窗口。

警告:下面的文字纯属猜测,但从我现在的经历来看,它似乎最有意义

因此,第一个表单似乎首先是TopMost,然后你移动另一个窗口,该窗口被排队为"你是下一个TopMost"。然后创建下一个表单,它也是TopMost,但它在"您是TopMost"队列中,位于您在这两个表单之间选择的窗口后面。

通过调用close以编程方式关闭窗体与使用红十字关闭窗体有些不同。单击鼠标使用红十字会产生额外的窗口消息,这些消息将激活流程的"下一个"窗口。你的应用程序仍然处于活动状态,因此它将重新激活下一个表单。看看窗口消息,你可以使用pinvokes。有关这些消息的更多信息,请访问pinvoke.net。

protected override void WndProc(ref Message m)
{
    Console.WriteLine(m.Msg);
    base.WndProc(ref m);
}

要解决您的问题,您可能需要在显示表单时调用activate。这将使新表单处于活动状态,从而成为TopMost。请记住,主动呼叫也会带来焦点。如果你想在不劫持焦点的情况下激活表单,你应该使用ShowWindow pinvoke。