1

I am trying to make a Windows Forms Application be able to repeatedly show and then hide itself.

The idea is to create a small app that overlays an image on the screen every time a modifier key like Num Lock or Caps lock is pressed. I have the detection of the keyboard keys working flawlessly, but I am not having much luck with creating a form that I can show and then hide repeatedly.

The way I see it, (please correct me if I'm wrong) there are two possible ways to make the form behave like I want it to:

  • Start the form normally in Program.cs and then hold the logic for hiding and showing the form and displaying the image inside Form1.cs
    • The form would call this.Hide() and this.Show() to hide and show itself, but whenever I try to do this, I cannot get this.Hide() to hide the form; the form remains visible on top of all of the open windows.
  • Hold the logic for hiding and showing the form in Program.cs and then just hold the logic to display the image in Form1.cs
    • I've been toying around with the code from this guide to show and hide the from from Program.cs with a class wrapper for the form, but as it turns out, form.Showdialog() prevents any further code execution until the form is closed.

I've been playing around with the code myself, and neither of the above methods have worked. Am I thinking about this in the wrong way entierly? Can a WFA ever behave in the way I want it to?

The code for this is a bit messy, and I'm not sure what parts are the most relevant here, but I'll do my best to include it here:

Program.cs:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Threading;
using System.Windows.Forms;

namespace KeyboardIndicators {
  // ApplicationContext wrapper
  public abstract class TrayIconApplicationContext : ApplicationContext {
    private NotifyIcon lockIcon;
    private ContextMenu lockIconContext;

    protected TrayIconApplicationContext() {
      // Wire up the ApplicationExitHandler to ApplicationExit events
      Application.ApplicationExit += this.ApplicationExitHandler;

      lockIconContext = new ContextMenu {

      };

      // Create lockIcon tray icon and make it visible
      lockIcon = new NotifyIcon {
        ContextMenu = lockIconContext,
        Text = Application.ProductName,
        Icon = new Icon("icon.ico"),
        Visible = true
      };

    }

    protected NotifyIcon LockIcon { get { return lockIcon; } }
    protected ContextMenu LockIconContext { get { return lockIconContext; } }


    // ApplicationExit event handler
    private void ApplicationExitHandler(object sender, EventArgs e) {
      this.OnApplicationExit(e);
    }

    // Performs cleanup to end the application
    protected virtual void OnApplicationExit(EventArgs e) {
      // TODO(Neil): Add meaningful thread cleanup here soon
      if (lockIcon != null) {
        lockIcon.Visible = false;
        lockIcon.Dispose();
      }
      if (lockIconContext != null)
        LockIconContext.Dispose();
    }
  }

  // TrayIconApplicationContext wrapper for Form1 to control the activation of the form window
  class FormApplicationContext : TrayIconApplicationContext {
    public FormApplicationContext() {
      // Add Exit menu item
      MenuItem exit = new MenuItem("E&xit");
      this.LockIconContext.MenuItems.Add(exit);
      exit.Click += this.ExitContextMenuClickHandler;

      //KeyboardIndicators indicators = new KeyboardIndicators();
      //indicators.RunListener();

      {
        using(Form form = new Form1("NumLock", true))
          form.ShowDialog();
      }
    }

    private void ExitContextMenuClickHandler(object sender, EventArgs eventArgs) {
      this.ExitThread();
    }
  }

  public class KeyboardIndicators {
    class LockState {
      // Is the numlock key on?
      public bool Num;
      // Is the capslock key on?
      public bool Caps;
      // Is the scroll lock key on?
      public bool Scroll;
    }

    public void RunListener() {
      try {
        // Store the old keyboard lock state
        LockState prevState = new LockState() {
          Num = Control.IsKeyLocked(Keys.NumLock),
          Caps = Control.IsKeyLocked(Keys.CapsLock),
          Scroll = Control.IsKeyLocked(Keys.Scroll)
        };

        while (true) {
          // Store the new keyboard lock state
          LockState newState = new LockState() {
            Num = Control.IsKeyLocked(Keys.NumLock),
            Caps = Control.IsKeyLocked(Keys.CapsLock),
            Scroll = Control.IsKeyLocked(Keys.Scroll)
          };

          //TODO(Neil): Handle simultaneous presses better, i.e. queue the balloon tips
          if (newState.Num != prevState.Num) {
            Form1 form = new Form1("NumLock", newState.Num);
          } else if (newState.Caps != prevState.Caps) {
            Form1 form = new Form1("CapsLock", newState.Caps);
          } else if (newState.Scroll != prevState.Scroll) {
            Form1 form = new Form1("ScrollLock", newState.Scroll);
          }

          // Set the previous lock state to the new one in prep for the next iteration
          prevState = newState;

          // Sleep for 500ms
          Thread.Sleep(500);
        }
      } catch (ThreadAbortException) { /* No need to do anything, just catch the ThreadAbortException.*/ }
    }
  }

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

      //Application.Run(new Form1("NumLock", true));
      Application.Run(new FormApplicationContext());
    }
  }
}

Form1.cs:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace KeyboardIndicators {
  public partial class Form1 : Form {
    public Form1(String activatedModifier, bool lockState) {
      InitializeComponent();
      ShowForm(activatedModifier);

      //this.Show();
      //this.Hide();
    }

    public void ShowForm(String activatedModifier) {
      PictureBox pictureBox = new PictureBox();

      Image myBitmap = Image.FromFile("cube.png");
      Size bitmapSize = new Size(myBitmap.Width, myBitmap.Height);

      switch (activatedModifier) {
        case "NumLock":
          break;
        case "CapsLock":
          break;
        case "ScrollLock":
          break;
      }

      this.Size = bitmapSize;
      pictureBox.ClientSize = bitmapSize;

      pictureBox.Image = myBitmap;
      pictureBox.Dock = DockStyle.Fill;
      this.Controls.Add(pictureBox);
      this.FormBorderStyle = FormBorderStyle.None;
    }

    protected override CreateParams CreateParams {
      get {
        CreateParams createParams = base.CreateParams;
        createParams.ExStyle |= 0x00000020; // WS_EX_TRANSPARENT

        return createParams;
      }
    }
  }
}
ifconfig
  • 6,242
  • 7
  • 41
  • 65
  • I noticed that you tried to call `this.Hide()` in the `Form1` constructor (the code is commented out). This does not work [see this SO answer](https://stackoverflow.com/questions/7003587/why-isnt-this-hide-working-in-form1-load-event). Are you able to hide the form if you call `this.Hide()` in the `Shown` event handler? – Marius Feb 28 '18 at 06:49
  • @Marius So I see. By the event handler, you mean `ShowForm()`? – ifconfig Feb 28 '18 at 06:51
  • No I mean the Shown event (see [msdn](https://msdn.microsoft.com/en-us/library/system.windows.forms.form.shown(v=vs.110).aspx)). You can invoke it e.g. like this `this.Shown += (s, e) => this.Hide();` – Marius Feb 28 '18 at 06:53
  • @Marius that specific code would just make it impossible to show, though... – Nyerguds Feb 28 '18 at 15:11
  • So, is the conclusion that what I'm asking isn't possible with a Windows Form Application, @Nyerguds and @Marius? – ifconfig Mar 01 '18 at 07:38
  • 1
    Of course it's possible. Just don't do your detection from the forms. Do them in the main program, and just show the applicable form whenever you detect the key. And either use ShowDialog and make a new form object each time, or make the form once and use show/hide on that one object. – Nyerguds Mar 01 '18 at 08:17
  • @Nyerguds Sure, but then I would run into the same issue I had before where the form would block further `Program.cs` execution while it was up. Would I just have the form destroy itself by `break`ing out? – ifconfig Mar 01 '18 at 21:21
  • So simply don't use ShowDialog then? Keep each form in its own variable, keep those stored in your program, and show/hide them whenever you want. – Nyerguds Mar 01 '18 at 22:16

1 Answers1

1

As an example I have created a Winforms Application which displays "NUM" and / or "CAPS" if any of those keys is pressed otherwise the form is hidden. The detection of the keys is based on the C# Low Level Keyboard Hook.

Program.cs

using System;
using System.Windows.Forms;

namespace WinFormsKeyHook
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
}

Form1.cs

Note: Add a label named 'label1' to Form1 in Designer.

using System.Collections.Generic;
using System.Windows.Forms;

namespace WinFormsKeyHook
{
    public partial class Form1 : Form
    {
        private static bool _caps;
        private static bool _num;

        public Form1()
        {
            InitializeComponent();

            KeyboardHook kh = new KeyboardHook();
            kh.KeysToObserve.AddRange(new List<Keys> { Keys.CapsLock, Keys.NumLock });
            kh.InstallHook();
            kh.KeyDown = key => ProcessKeyDown(key);

            _caps = Control.IsKeyLocked(Keys.CapsLock);
            _num = Control.IsKeyLocked(Keys.NumLock);
        }

        private void ProcessKeyDown(Keys key)
        {

            if (key == Keys.CapsLock)
            {
                _caps = !_caps;
            }
            if (key == Keys.NumLock)
            {
                _num = !_num;
            }

            this.ShowState(_num, _caps);
        }

        internal void ShowState(bool num, bool caps)
        {
            if (!num && !caps)
            {
                this.Hide();
                return;
            }

            this.label1.Text = "";
            this.label1.Text += num ? "NUM " : "";
            this.label1.Text += caps ? "CAPS" : "";

            if (!this.Visible)
            {
                this.Show();
            }
        }
    }
}

KeyboardHook.cs

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace WinFormsKeyHook
{
    public class KeyboardHook
    {
        private const int WH_KEYBOARD_LL = 13;
        private const int WM_KEYDOWN = 0x0100;
        private IntPtr _hookID = IntPtr.Zero;

        public List<Keys> KeysToObserve { get; set; } = new List<Keys>();

        public Action<Keys> KeyDown;

        public void InstallHook()
        {
            _hookID = SetHook(HookCallback);
        }

        ~KeyboardHook()
        {
            UnhookWindowsHookEx(_hookID);
        }

        public IntPtr SetHook(LowLevelKeyboardProc proc)
        {
            using (Process curProcess = Process.GetCurrentProcess())
            using (ProcessModule curModule = curProcess.MainModule)
            {
                return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0);
            }
        }

        public delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);

        public IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
        {
             if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
            {
                int vkCode = Marshal.ReadInt32(lParam);
                var key = (Keys)vkCode;
                Console.WriteLine(key);
                KeyDown(key);
            }

            return CallNextHookEx(_hookID, nCode, wParam, lParam);
        }

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool UnhookWindowsHookEx(IntPtr hhk);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr GetModuleHandle(string lpModuleName);
    }
}
Marius
  • 1,529
  • 6
  • 21
  • What exactly is the purpose of the `KeyboardHook` class? Is the point that the form blocks on the thread and `KeyboardHook` is required to cause an interrupt to change the form text? – ifconfig Mar 01 '18 at 21:09
  • In my opinion using `Thread.Sleep()` in a `while` loop is not good practice [see this SO post for a discussion](https://stackoverflow.com/questions/8815895/why-is-thread-sleep-so-harmful). I think what you want to achieve is to react to keyboard input and that is better reflected by hooking to an event. You could also use a timer which fires every 500ms instead of the keyboard hook. I just wanted to give you a working example and it took me less time to do with the keyboard hook because I already knew how it works. – Marius Mar 02 '18 at 00:53