It is possible to get any keystrokes in application regardless of active window?

248 views Asked by At

My MDI application handles regular keystrokes (A-Z, 0-9 etc.) in handler of Main window with KeyPreview = true. It filters some sequences of incoming characters, hiding them from subsequent KeyPress handlers. This works fine in MDI windows, but the Main window's KeyPress is not invoked if modal window is active (because Main form is no longer focused). I open modal windows through simple ShowDialog().

Is there some way to universally catch and filter KeyPress events regardless of which application window has the focus (including modal windows?)

I'm searching for handling of KeyPresses on application level, not global level. => I don't want to be notified of KeyPresses if the application doesn't have focus.

3

There are 3 answers

4
Idle_Mind On BEST ANSWER

Is there some way to universally catch and filter KeyPress events regardless of which application window has the focus (including modal windows?) I'm searching for handling of KeyPresses on application level ...

This can be accomplished via IMessageFilter().

Here's a quick example:

public partial class MainMdiParent : Form
{

    private MyFilter MF = new MyFilter();

    public MainMdiParent()
    {
        InitializeComponent();
    }

    private void MainMdiParent_Load(object sender, EventArgs e)
    {
        MF.EscapeKey += MF_EscapeKey;
        Application.AddMessageFilter(MF);

        childA a = new childA();
        a.MdiParent = this;
        a.Show();

        a = new childA();
        a.MdiParent = this;
        a.Show(); 
    }

    private void MF_EscapeKey()
    {
        Console.WriteLine("Escape Key Trapped in Main Form");
    }

    private void button1_Click(object sender, EventArgs e)
    {
        Form dlg = new Form();
        dlg.ShowDialog();
    }

}

public class MyFilter : IMessageFilter
{

    public delegate void EscapeKeyDelegate();
    public event EscapeKeyDelegate EscapeKey;

    private const int WM_KEYDOWN = 0x100;

    public bool PreFilterMessage(ref Message m)
    {
        switch (m.Msg )
        {
            case WM_KEYDOWN:
                switch (m.WParam.ToInt32())
                {
                    case (int)Keys.Escape:
                        if (EscapeKey != null)
                        {
                            EscapeKey();
                        }
                        return true; // suppress it?
                        break;
                }
                break;

        }

        return false; // returning false allows messages to be processed normally
    }

}
4
George mits On

according to msdn the system sends keyboard messages only to the foreground thread, does the modal windows start in another thread ? if yes you can just message the main from the modal window thread with some simple thread messaging using the same handles you used in the main window form , now if you don't have any idea of what thread is .... you can always learn

here are some nice articles that can help you about keyboards

about threads and msgs

additional note: you can always use sendkeys function to send keys to another target , if needed

Edit: Showdialog is a blocking asynchronous call , it "freezes" the thread and other windows and returns only when the window that launched was closed , this is why the handles don't respond when you have modal window activated

1
caesay On

The problem is, that when you call ShowDialog, your main UI thread will stop dispatching messages. Look at this simplified message loop:

while(true) 
{
    Message m;
    GetMessage(out m);
    // if close button pressed etc.
    if (m.Msg == WM_QUIT) break;
    DispatchMessage(m);
}

When you activate your dialog, this is executed in DispatchMessage and starts a new message loop that is similar. Because of this new loop, the loop from your main window is blocked on that DispatchMessage call, and will not process any keyboard messages. It is tricky working with multiple UI loops and for that reason I would recommend using the regular Show() message (which will not block your main application loop) and find another way to direct the users focus to that window.

There is a p/invoke call that I have used in the past which sets the parent window as disabled and directs user focus to the child window, it is almost identical to the behavior of the ShowDialog call, but without blocking the thread. What you would do is set the owner of your child form, call the regular Show() method, and finally set the enabled state of your parent window to false. You can do the later with this code:

const int GWL_STYLE   = -16;
const int WS_DISABLED = 0x08000000;

[DllImport("user32.dll")]
static extern int GetWindowLong(IntPtr hWnd, int nIndex);

[DllImport("user32.dll")]
static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);

void SetWindowEnabled(Form form, bool enabled)
{
    SetWindowLong(form.Handle, GWL_STYLE, GetWindowLong(form.Handle, GWL_STYLE) &
                  ~WS_DISABLED | (enabled ? 0 : WS_DISABLED));
}

Touching on your comment about stacked model windows, I think this is possible but it is important to make sure that the native method is called to re-enable the parent window when the dialog is closed. I would implement an extension method that looks something like this:

public static class FormsExtensions
{
    public static Task<bool> ShowNativeDialog(this Form child, Form owner)
    {
        var tcs = new TaskCompletionSource<bool>();
        child.Show();
        SetWindowEnabled(owner, false);
        child.Closed += (sender, args) => { 
            SetWindowEnabled(owner, true);
            tcs.SetResult(true);
        }
        return tcs.Task;
    }
}

And usage would look something like this:

DialogForm dialog = new DialogForm();
await dialog.ShowNativeDialog(this);

By using await, you can stop the execution flow without blocking the UI messaging loop.