Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Permanent focus for tool window

Tags:

c#

winforms

Is there any way of creating a tool window in WinForms that as long as the hosting form has focus, the tool window does as well? An example of this is in Paint.NET:

Focused tool window

I'm using C# as the backend application language, under .Net 4.0.

like image 261
Darkzaelus Avatar asked May 23 '13 09:05

Darkzaelus


2 Answers

The source code for an old version of Paint.Net is available at openpdn Fork of Paint.NET 3.36.7

I tried to extract their methods from that source code into the most concise working example I could muster:

Pinvoking class:

internal static class Win32 {
  public const int WM_ACTIVATE = 0x006;
  public const int WM_ACTIVATEAPP = 0x01C;
  public const int WM_NCACTIVATE = 0x086;

  [DllImport("user32.dll", SetLastError = false)]
  internal static extern IntPtr SendMessageW(IntPtr hWnd, uint msg,
                                             IntPtr wParam, IntPtr lParam);

  [DllImport("user32.dll", SetLastError = true)]
  [return: MarshalAs(UnmanagedType.Bool)]
  internal extern static bool PostMessageW(IntPtr handle, uint msg,
                                           IntPtr wParam, IntPtr lParam);
}

Base Form:

public partial class Form1 : Form {

  public Form1() {
    InitializeComponent();
  }

  private bool ignoreNcActivate = false;

  protected override void WndProc(ref Message m) {
    base.WndProc(ref m);

    switch (m.Msg) {
      case Win32.WM_NCACTIVATE:
        if (m.WParam == IntPtr.Zero) {
          if (ignoreNcActivate) {
            ignoreNcActivate = false;
          } else {
            Win32.SendMessageW(this.Handle, Win32.WM_NCACTIVATE, new IntPtr(1), IntPtr.Zero);
          }
        }
        break;
      case Win32.WM_ACTIVATEAPP:
        if (m.WParam == IntPtr.Zero) {
          Win32.PostMessageW(this.Handle, Win32.WM_NCACTIVATE, IntPtr.Zero, IntPtr.Zero);
          foreach (Form2 f in this.OwnedForms.OfType<Form2>()) {
            f.ForceActiveBar = false;
            Win32.PostMessageW(f.Handle, Win32.WM_NCACTIVATE, IntPtr.Zero, IntPtr.Zero);
          }
          ignoreNcActivate = true;
        } else if (m.WParam == new IntPtr(1)) {
          Win32.SendMessageW(this.Handle, Win32.WM_NCACTIVATE, new IntPtr(1), IntPtr.Zero);
          foreach (Form2 f in this.OwnedForms.OfType<Form2>()) {
            f.ForceActiveBar = true;
            Win32.SendMessageW(f.Handle, Win32.WM_NCACTIVATE, new IntPtr(1), IntPtr.Zero);
          }
        }
        break;
    }
  }

  protected override void OnShown(EventArgs e) {
    base.OnShown(e);
    Form2 f = new Form2();
    f.Show(this);
  }
}

Always active Form2 (unless app is not active):

public partial class Form2 : Form {
  internal bool ForceActiveBar { get; set; }

  public Form2() {
    InitializeComponent();
    this.ShowInTaskbar = false;
    this.ForceActiveBar = true;
  }

  protected override void WndProc(ref Message m) {
    base.WndProc(ref m);
    if (m.Msg == Win32.WM_NCACTIVATE) {
      if (this.ForceActiveBar && m.WParam == IntPtr.Zero) {
        Win32.SendMessageW(this.Handle, Win32.WM_NCACTIVATE, new IntPtr(1), IntPtr.Zero);
      }
    }
  }
}

There is no need to set TopMost to true for Form2 since it should be owned by the main form when it gets displayed. Also, Form2 is not an MDI child form.

like image 194
LarsTech Avatar answered Sep 28 '22 18:09

LarsTech


The tool windows in Paint.NET are just that—tool windows. In Win32 terms, you achieve this by creating the window with the WS_EX_TOOLWINDOW extended window style:

The window is intended to be used as a floating toolbar. A tool window has a title bar that is shorter than a normal title bar, and the window title is drawn using a smaller font. A tool window does not appear in the taskbar or in the dialog that appears when the user presses ALT+TAB.

In WinForms, this is controlled by the FormBorderStyle property. Set it to either FormBorderStyle.FixedToolWindow or FormBorderStyle.SizableToolWindow in your form's constructor.

You also need to make sure that you specify an owner window for the tool window. Its owner should be your main form, the one for which it serves as a tool palette. You generally do this when showing the form, using the overload of the Show method that allows you to specify an owner window.

Finally, another cool effect that Paint.NET has (I think, if I remember correctly) is that the tool windows can never actually receive the focus. You can interact with them, clicking on buttons to select tools, but you can't actually set the focus to a floating palette. It always goes back to the main window. A naive attempt to emulate this behavior might be to reset the focus in one of the focus-changing notifications (e.g., the Activate event), but that's not a good idea for numerous reasons. A better solution would be to add the WS_EX_NOACTIVATE extended window style. I'm not aware of any property that exposes this functionality in WinForms, but you can set it manually during the window's creation by overriding the CreateParams property. For example:

public class MyForm : Form
{
    // ... other code ...

    protected override CreateParams CreateParams {
        get {
            const int WS_EX_NOACTIVATE = 0x08000000;

            CreateParams cp = base.CreateParams;
            cp.ExStyle |= WS_EX_NOACTIVATE;
            return cp;
        }
     }
}
like image 24
Cody Gray Avatar answered Sep 28 '22 19:09

Cody Gray