C# Winforms系统托盘应用程序:如何通过托盘事件打开/关闭或显示/隐藏窗体?

huangapple go评论62阅读模式
英文:

C# Winforms Tray App: How to open/close or show/hide a form via tray event?

问题

我有一个托盘应用程序,带有事件处理程序。每当触发事件时,我希望通过相应的弹出图像通知用户有关状态。这个图像应该在屏幕中央显示约500毫秒,我需要一个带有图片框的窗体。

我尝试过通过新建和关闭以及通过显示和隐藏来显示窗体,但两者都没有按预期工作。要么窗体只在开始时(通过上下文类的构造函数创建和显示它时)显示和隐藏一次,而不是为进一步的事件触发(我可以看到窗体的边界,但是它是灰色的并且挂起),要么窗体根本不显示(当我仅通过事件处理程序的委托从中上下文类的方法创建和显示它时),要么只能看到半毫秒,或者在第二个触发发生时出现线程错误(即使我没有使用任何额外的线程)。

我真的很迷茫,不知道哪种方法是正确的。

更新和解决方案:

由于我使用的是托盘应用程序(从ApplicationContext而不是Form开始),必须手动处理UI线程,请参见在Winform应用程序中在没有窗体或控件的情况下调用UI线程的方法

如顶部答案中所述,您需要添加一个用于Application.Idle事件的方法,在其中放置您的控制器/处理程序的实例化,以防止重复实例化,从而导致UI线程死锁。

对于实例化以及在控制器/处理程序类中,您需要添加一个Action<Action>类型的UI调用引用,该引用可用于任何UI操作,因为这些操作直接在UI线程中执行。

英文:

I have a tray app with an event handler. Whenever the event is triggered I want to inform the user via a corresponding popup image about a status. This image should appear for around 500 ms in the center of the screen for which I need a form with a picturebox.

I tried to display the form via new & close and via show & hide but both are not working as expected. Either the form is showing and hiding itself only once at the start (when I create and show it via the constructor of the context class) but not for further event triggers (I can see the forms boundaries but its grey and hanging) or the form is not showing at all (when I only create and show it via delegate from the event handler to a method of the context class) or it is only seen for half a millisecond or I get a thread error (even when I do not use any additional threads) when the second trigger happens.

I am really lost here and do not know which would be the correct approach.

UPDATE & SOLUTION:

As I use a tray app (starting with an ApplicationContext instead of a Form) the UI thread has to be handled manually, see How to invoke UI thread in Winform application without a form or control.

As explained in the top answer, you need to add a method for the Application.Idle event where you put the instanciation of your controller/handler in order to prevent duplicate instanciations and thereby a deadlock of your UI thread.

For the instanciation and within the controller/handler class you need to add a UI invoke reference of the type Action&lt;Action&gt; which can be used for any UI manipulations as those are directly executed in the UI thread.

答案1

得分: 0

最终代码符合在 OP 下的解释和更新:

Program.cs 是我们托盘应用的主入口点:

    using System;
    using System.Windows.Forms;

    namespace Demo {
      static class Program {
        [STAThread]
        static void Main() {
          Application.EnableVisualStyles();
          Application.SetCompatibleTextRenderingDefault(false);
          Application.Run(new MyContext()); // 我们从上下文开始,而不是从表单开始
        }
      }
    }

MyContext.cs 通过 Application.Idle 事件方法防止重复实例化,并具有一个 Update() 方法,其中使用处理程序值来更新一些 UI 元素:

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

    namespace Demo {
      public class MyContext : ApplicationContext {
        private MyHandler  _Handler  = null;
        private NotifyIcon _TrayIcon = null;
    
        public MyContext() {
          // 构造函数在 OnApplicationIdle 方法内
          // 由于 UI 线程处理和在事件发生时防止重复
          Application.ApplicationExit += new EventHandler(OnExit);
          Application.Idle            += new EventHandler(OnIdle);
        }

        new public void Dispose() {
          _TrayIcon.Visible = false;
         Application.Exit();
        }    

        private void OnExit(object sender, EventArgs e) {
          Dispose();
        }
    
        private void OnIdle(object sender, EventArgs e) {
          // 防止在每个 Idle 事件上重复初始化
          if (_Handler == null) {
            var context = TaskScheduler.FromCurrentSynchronizationContext();
    
            _Handler = new MyHandler(
              (f) => {                      // MyHandler 构造函数的第一个参数
                Task.Factory.StartNew(
                  () => {
                    f();
                  },
                  CancellationToken.None,
                  TaskCreationOptions.None,
                  context);
              },
              this                          // MyHandler 构造函数的第二个参数
            );
  
            _TrayIcon     = new NotifyIcon() {
              ContextMenu = new ContextMenu(new MenuItem[] {
                new MenuItem("切换某事", ToggleSomething),
                new MenuItem("-"),          
                new MenuItem("退出",             OnExit)
              }),
              Text        = "我的精彩应用",
              Visible     = true
            };
    
            _TrayIcon.MouseClick += new MouseEventHandler(_TrayIcon_Click);
    
            Update();                       // 使用处理程序,显示表单
          }
        }
    
        public void Update() {
         bool value = _Handler.GetValue();

          // 托盘图标被更新
          _TrayIcon.Icon = value ? path.to.icon.when.true
                                 : path.to.icon.when.false;

          // 表单显示后自行关闭
          MyForm form = new MyForm(value);
          form.Show();
        }
    
        private void _TrayIcon_Click(object sender, MouseEventArgs e) {
          if (e.Button == MouseButtons.Left) {
            ToggleSomething(sender, e);
          }
        }

        private void ToggleSomething(object sender, EventArgs e) {
          _Handler.ToggleValue();
        }

        // ...
      }
    }

MyHandler.cs 需要一个 UI 调用引用,通过它可以直接调用 UI 线程并从而操作 UI 元素:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    // ...

    namespace Demo {
      public class MyHandler {
        private          MyContext      _Context     = null;
        private readonly Action<Action> _UIInvokeRef = null;
        private          bool           _Value       = false;

        public MyHandler(Action<Action> uIInvokeRef, MyContext context) {
          _Context          = context;
          _UIInvokeRef      = uIInvokeRef;

          // ...
          Something something.OnSomething += Something_OnSomething; // 由外部触发的事件(例如,对系统设备做出反应的库)
        }

        private void Something_OnSomething(Data data) {
          _Value = data.Value > 10 ? true : false;  // 数据已更改并设置了值

          // ...

          _UIInvokeRef(() => {                      // 使用 UI 线程
            _Context.Update();                      // 更新托盘图标并显示表单
          });
        }

        // ...

        public bool GetValue() {
          return _Value;
        }

        public void ToggleValue() {
          _Value = !_Value;

          // 也可用于操纵系统设备(例如)
          // 以触发 Something_OnSomething 事件
          // 然后更新 UI 元素
        }
      }
    }

MyForm.cs 使用定时器,可以关闭自己:

    using System;
    using System.Windows.Forms;

    namespace Demo {
      public partial class MyForm : Form {
        private System.Windows.Forms.Timer _Timer = null;

        public FormImage(bool value) {
          InitializeComponent();

          pbx.Image = value ? path.to.picture.when.true
                            : path.to.picture.when.false;
        }

        protected override void OnLoad(EventArgs e) {
          base.OnLoad(e);

          this.FormBorderStyle = FormBorderStyle.None;
          this.StartPosition   = FormStartPosition.CenterScreen;
          this.ShowInTaskbar   = false;
          this.TopLevel        = true;
        }

        protected override void OnShown(EventArgs e) {
          base.OnShown(e);

          _Timer = new System.Windows.Forms.Timer();
          _Timer.Interval = 500;                       // 定时器触发事件的时间间隔
          _Timer.Tick += new EventHandler(Timer_Tick); // 注册定时器触发事件
          _Timer.Start();                              // 启动定时器
        }

        private void Timer_Tick(object sender, EventArgs e) {
          _Timer.Stop();                               // 停止定时器
          _Timer.Dispose();                            // 丢弃定时器
          this.Close();                                // 自行关闭表单
        }
      }
    }

即使处理程序事件(系统触发)Something_OnSomething 的调用速度比表单定时器事件 Timer_Tick 更快,这也能正常工作。

英文:

Final code fitting to the explanation in the OP under UPDATE & SOLUTION:

Program.cs is the main entry point of our tray app:

using System;
using System.Windows.Forms;
namespace Demo {
static class Program {
[STAThread]
static void Main() {
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MyContext()); // we start with context instead of form
}
}
}

MyContext.cs prevents duplicate instanciation via Application.Idle event method and has an Update() method where it uses a handler value to update some UI elements:

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;    
namespace Demo {
public class MyContext : ApplicationContext {
private MyHandler  _Handler  = null;
private NotifyIcon _TrayIcon = null;
public MyContext() {
// constructor is within the OnApplicationIdle method
// due to UI thread handling and preventing duplicates when having events
Application.ApplicationExit += new EventHandler(OnExit);
Application.Idle            += new EventHandler(OnIdle);
}
new public void Dispose() {
_TrayIcon.Visible = false;
Application.Exit();
}    
private void OnExit(object sender, EventArgs e) {
Dispose();
}
private void OnIdle(object sender, EventArgs e) {
// prevent duplicate initialization on each Idle event
if (_Handler == null) {
var context = TaskScheduler.FromCurrentSynchronizationContext();
_Handler = new MyHandler(
(f) =&gt; {                      // 1st parameter of MyHandler constructor
Task.Factory.StartNew(
() =&gt; {
f();
},
CancellationToken.None,
TaskCreationOptions.None,
context);
},
this                          // 2nd parameter of MyHandler constructor
);
_TrayIcon     = new NotifyIcon() {
ContextMenu = new ContextMenu(new MenuItem[] {
new MenuItem(&quot;Toggle Something&quot;, ToggleSomething),
new MenuItem(&quot;-&quot;),          
new MenuItem(&quot;Exit&quot;,             OnExit)
}),
Text        = &quot;My wonderful app&quot;,
Visible     = true
};
_TrayIcon.MouseClick += new MouseEventHandler(_TrayIcon_Click);
Update();                       // Handler is used and form is shown
}
}
public void Update() {
bool value = _Handler.GetValue();
// tray icon is updated
_TrayIcon.Icon = value ? path.to.icon.when.true
: path.to.icon.when.false;
// form is shown and closed by itself after a particular amount of time 
MyForm form = new MyForm(value);
form.Show();
}
private void _TrayIcon_Click(object sender, MouseEventArgs e) {
if (e.Button == MouseButtons.Left) {
ToggleSomething(sender, e);
}
}
private void ToggleSomething(object sender, EventArgs e) {
_Handler.ToggleValue();
}
// ...
}
}

MyHandler.cs needs a UI invoke reference by which it can directly call into the UI thread and thereby manipulate UI elements:

using System;
using System.Collections.Generic;
using System.Linq;
// ...
namespace Demo {
public class MyHandler {
private          MyContext      _Context     = null;
private readonly Action&lt;Action&gt; _UIInvokeRef = null;
private          bool           _Value       = false;
public MyHandler(Action&lt;Action&gt; uIInvokeRef, MyContext context) {
_Context          = context;
_UIInvokeRef      = uIInvokeRef;
// ...
Something something.OnSomething += Something_OnSomething; // an event that is triggered by something outside (e.g. a library that reacts to a system device)
}
private void Something_OnSomething(Data data) {
_Value = data.Value &gt; 10 ? true : false;  // data has been changed and value is set
// ...
_UIInvokeRef(() =&gt; {                      // UI thread is used
_Context.Update();                      // update tray icon and show form
});
}
// ...
public bool GetValue() {
return _Value;
}
public void ToggleValue() {
_Value = !_Value;
// can also be used to manipulate a system device (e.g.)
// in order to trigger the Something_OnSomething event
// which then updates the UI elements
}
}
}

MyForm.cs uses a timer by which it can close itself:

using System;
using System.Windows.Forms;
namespace Demo {
public partial class MyForm : Form {
private System.Windows.Forms.Timer _Timer = null;
public FormImage(bool value) {
InitializeComponent();
pbx.Image = value ? path.to.picture.when.true
: path.to.picture.when.false;
}
protected override void OnLoad(EventArgs e) {
base.OnLoad(e);
this.FormBorderStyle = FormBorderStyle.None;
this.StartPosition   = FormStartPosition.CenterScreen;
this.ShowInTaskbar   = false;
this.TopLevel        = true;
}
protected override void OnShown(EventArgs e) {
base.OnShown(e);
_Timer = new System.Windows.Forms.Timer();
_Timer.Interval = 500;                       // intervall until timer tick event is called
_Timer.Tick += new EventHandler(Timer_Tick); // timer tick event is registered
_Timer.Start();                              // timer is started
}
private void Timer_Tick(object sender, EventArgs e) {
_Timer.Stop();                               // timer is stopped
_Timer.Dispose();                            // timer is discarded
this.Close();                                // form is closed by itself
}
}
}

This works even when the handler event (system trigger) Something_OnSomething is called faster again than the form timer event Timer_Tick can close the form.

huangapple
  • 本文由 发表于 2023年4月13日 22:11:47
  • 转载请务必保留本文链接:https://go.coder-hub.com/76006457.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定