1

I have a main form with two child modeless forms, e.g. all of the forms can be active simultaneously:

class MainForm : Form
{
    Form child1;
    Form child2;

    public MainForm()
    {
        Text = "MainForm";
        child1 = new Form { Text = "Child1" };
        child2 = new Form { Text = "Child2" };
        child1.Show(this);
        child2.Show(this);
    }
}

I would like to allow the user to Alt+Tab into all of them, but surprisingly, I found that if any of the child forms is active, the owner form cannot be selected from the Alt+Tab menu.

All three forms show up in the list, but apparently when you select the owner window and there is an active child, the child gets selected rather than the owner. The same thing happens when selecting forms in the taskbar.

Am I missing something? I started thinking of explicitly configuring shortcut keys to allow navigating from the modeless child form to the owner window, but before doing this I wanted to confirm if there is already some built-in keyboard shortcut to do this, since I don't want to break users' expectations.

Surprisingly, I couldn't find any question mentioning this behavior, which I also found rather odd.

Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
glopes
  • 4,038
  • 3
  • 26
  • 29
  • Are you setting the owner (`.Show(this);`) so the new Form stays on top of it? If this is the desired behaviour, than you get what you asked for. If you don't want this behaviour, don't set the owner. – Jimi Feb 24 '19 at 17:20
  • I believe the code snippet I included in the text addresses your first question. Regarding the comment "you get what you asked for", can you elaborate on that? Given that *modeless* dialogs can all be independently activated using the mouse, why can they not be independently activated using the keyboard? It seems to me like an obvious accessibility issue (i.e. users without access to keyboard, or invisual, cannot access the forms). – glopes Feb 24 '19 at 17:33
  • You can always override `ProcessCmdKey`, check whether a user pressed `CTRL + F6`, and pass the focus to the Owner: `this.Owner.Focus()`. You could of course use any combination or a single key, that's just the default. When you ALT + TAB, the child is on top of its owner, so it's the Window that receives the focus. – Jimi Feb 24 '19 at 17:49
  • Yes, I was suspecting that I would have to setup my own shortcut key mechanism for this. May I ask why `CTRL+F6`? Is that a Windows standard / recommendation for moving from child window to its owner? I'm asking because it is important to know if experienced Windows users will expect this, or if it will be something that will have to be explicitly taught. – glopes Feb 24 '19 at 17:54
  • Well, in Visual Studio, open up some files and press `Ctrl+F6` and you'll see. The same for other programs. That's a default shortcut to move the focus to different child windows (or the owner's). – Jimi Feb 24 '19 at 17:56
  • Fair enough, if you post this as an answer, I would upvote you, thanks. – glopes Feb 24 '19 at 17:58
  • Ah, well, sure. I'll post an example on how to switch from a Child window to the Owner's and back. As soon as I have some free time (if someone doesn't come by in the meanwhile). – Jimi Feb 24 '19 at 18:50
  • Personally, I don't see the point of this design. Why offer the user the ability to tab to the main window when you want the child windows always to be on top? It sounds to me like you're trying to create an MDI application but you want the MDI windows to be outside of the parent window. You're mixing modal/model/MDI UI metaphors here, and it makes for a rather unorthodox requirement. – Jazimov Feb 24 '19 at 22:17
  • @Jazimov I think your comment goes beyond the scope of the question, and I have no time to address it here. What IS in scope of the question is that Windows allows modeless windows, with an owner, to be activated independently by the mouse, but not by keyboard. This is inconsistent from an accessibility point of view. – glopes Feb 24 '19 at 23:02
  • Ah, point well-taken. Your reply clarifies what your question really is about, and I totally agree that it's very odd that the keyboard Alt-Tab option cannot successfully activate the main window while the mouse can... Odd indeed. – Jazimov Feb 25 '19 at 00:43

2 Answers2

2

Setting the Owner of a Form, causes this Form to stay on top of its Owner as a non-modal Window.
If the owned Form has its ShowInTaskbar property set to true, the standard ALT+TAB or WIN+TAB combination of keys used to iterate the opened Windows in the System, brings to front (activates) the next owned Form instead of the Owner.
Which child Form is activated, it depends on the current position of the Form in the Taskbar.

If the ShowInTaskbar property of the children is instead set to false, the Owner Form is activated.
To note that if a child Form can be minimized, some awkward behaviour can be observed: Alt or Control-tabbing, cause the child Forms to appear and disappear in an unpleasant way.

Anyway, the standard combination of CONTROL+F6 keys can be used to move the focus on the opened child Forms (and the Owner Form, in this kind of layout), also bringing them to front if necessary (if the Form is minimized).
Here, overridng ProcessCmdKey, so the combination of keys is intercepted no matter what child control has captured the cursor.

The code here activates a Form pressing both CONTROL+F6 and CONTROL+SHIFT+F6, moving the Focus to each of the opened child Forms and the Owner. It also works when a child Form is minimized (or all the them).

In the Owner Form:

protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
    bool isControlF6 = keyData == (Keys.Control | Keys.F6);
    bool isCtrlShiftF6 = keyData == (Keys.Control | Keys.Shift | Keys.F6);

    if (isControlF6 || isCtrlShiftF6)
    {
        Form frm = isCtrlShiftF6 
                 ? Application.OpenForms.OfType<Form>().LastOrDefault(f => f.Owner == this)
                 : Application.OpenForms.OfType<Form>().FirstOrDefault(f => f.Owner == this);
        if (frm is null) return true;
        frm.WindowState = FormWindowState.Normal;
        frm.Focus();
        return true;
    }
    return base.ProcessCmdKey(ref msg, keyData);
}

In the child Forms:

protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
    bool isControlF6 = keyData == (Keys.Control | Keys.F6);
    bool isCtrlShiftF6 = keyData == (Keys.Control | Keys.Shift | Keys.F6);
    if (isControlF6 || isCtrlShiftF6) {
        int frmNext = 0;
        var formsList = Application.OpenForms.OfType<Form>()
                                   .Where(f => (f.Owner == this.Owner) || (f == this.Owner)).ToList();
        for (int i = 0; i < formsList.Count; i++) {
            if (formsList[i] == this) {
                if (isCtrlShiftF6) { frmNext = i == 0 ? formsList.Count - 1 : i - 1; }
                if (isControlF6) { frmNext = i == formsList.Count - 1 ? 0 : i + 1; }
                formsList[frmNext].WindowState = FormWindowState.Normal;
                formsList[frmNext].Focus();
                return true;
            }
        }
    }
    return base.ProcessCmdKey(ref msg, keyData);
}
Jimi
  • 29,621
  • 8
  • 43
  • 61
0

Edited answer:

I don't know why but I just couldn't let this go. It seemed there should be an easy solution.

@glopes I believe this is what you are looking for based on your comment.

This code will set the focus back to the parent window just before the child window looses focus. This acts similar to clicking on the window and allows you to Alt-Tab to any window you want.

public class MainForm : Form
{
    Form child1;
    Form child2;

    public MainForm()
    {
        Text = "MainForm";
        child1 = new ChildForm { Text = "Child1", ParentPtr = Handle };
        child2 = new ChildForm { Text = "Child2", ParentPtr = Handle };
        child1.Show(this);
        child2.Show(this);
    }
}

public class ChildForm : Form
{
    [DllImport("user32.dll")]
    public static extern bool SetFocus(IntPtr hWnd);

    private const int WM_KILLFOCUS = 0x0008;

    public IntPtr ParentPtr { get; set; }

    protected override void WndProc(ref Message m)
    {
        if (m.Msg == WM_KILLFOCUS) SetFocus(ParentPtr);

        base.WndProc(ref m);
    }
}
Jerry
  • 1,477
  • 7
  • 14
  • Unfortunately, this does not work for me, as I need the child windows to always be on top of the parent window, although they can be independently activated, i.e. *modeless* rather than *modal* dialogs. – glopes Feb 24 '19 at 17:34
  • The updated answer is very close. Unfortunately, it now prevents independent activation using the mouse. If you select `child1` followed by `child2` (with the mouse), you end up selecting the main form because of the handling order of focus messages. – glopes Feb 24 '19 at 20:55
  • I see what you mean, if you click on the title bar it works as intended. When you click in the window area it does not. – Jerry Feb 24 '19 at 21:26