Search code examples
c#winformsevent-handlingtray

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


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<Action> which can be used for any UI manipulations as those are directly executed in the UI thread.


Solution

  • 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) => {                      // 1st parameter of MyHandler constructor
                Task.Factory.StartNew(
                  () => {
                    f();
                  },
                  CancellationToken.None,
                  TaskCreationOptions.None,
                  context);
              },
              this                          // 2nd parameter of MyHandler constructor
            );
    
            _TrayIcon     = new NotifyIcon() {
              ContextMenu = new ContextMenu(new MenuItem[] {
                new MenuItem("Toggle Something", ToggleSomething),
                new MenuItem("-"),          
                new MenuItem("Exit",             OnExit)
              }),
              Text        = "My wonderful app",
              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<Action> _UIInvokeRef = null;
        private          bool           _Value       = false;
    
        public MyHandler(Action<Action> 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 > 10 ? true : false;  // data has been changed and value is set
    
          // ...
    
          _UIInvokeRef(() => {                      // 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.