Keiki, my OptusNet Usage Meter, is designed to sit in the notification area (or system tray, if you prefer) and behave similarly to the default system ‘applets’ (Volume/Network/Action Centre/Power). That is, the application becomes visible with a single left click on the notify (tray) icon, and is hidden again when focus is lost.
I have recently started to refactor Keiki’s code, as I have learnt a lot about WPF since I first wrote the application. While refactoring I’ve also tried to polish a few rough edges: one of these is the main window’s resize behaviour. The Windows 7 (and Vista before it) tray applications (excuse the ever-changing terminology) have no title bar and are not resizable. It turns out that this window style isn’t trivial to implement with WPF.
Updated: improved code to focus application when border is clicked.
Intuitively, we would simply set WindowStyle to ‘None’ and ResizeMode to ‘NoResize’:
1 |
<Window ... WindowStyle="None" ResizeMode="NoResize"> |
However, this results in a window without a border at all. Using ‘CanResize’ as the ResizeMode gives us the correct appearance, but now we’re stuck with a resizable window.
My earlier solution (as found in the current version of Keiki) was to simply set the window’s MinWidth/MinHeight and MaxWidth/MaxHeight to the same value when the window is loaded:
1 2 3 4 |
this.MinWidth = this.ActualWidth; this.MaxWidth = this.ActualWidth; this.MinHeight = this.ActualHeight; this.MaxHeight = this.ActualHeight; |
This has a few drawbacks:
- The resize cursors still appear when the mouse is over the window border.
- In Windows 7, Aero Snap still kind-of works, resulting in some strange behaviour (the window moves to the top right corner of the screen).
Fortunately, Dave Mullaney has a better solution, which I will reproduce here with some changes.
Firstly, set the window’s WindowStyle to ‘None’ and ResizeMode to ‘CanResize’.
We now need to add some code-behind that handles the messages sent to our window. I will try not to embarrass myself by displaying my level of ignorance regarding the Win32 API and leave it at that.
Simply copy the following into your window’s code-behind:
1 2 |
using System.Windows.Interop; using System.Runtime.InteropServices; |
…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
#region Disable Window Resize protected override void OnSourceInitialized(EventArgs e) { base.OnSourceInitialized(e); HwndSource source = PresentationSource.FromVisual(this) as HwndSource; source.AddHook(WndProc); } // system constants private const int WM_NCHITTEST = 0x0084; private const int WM_SETCURSOR = 0x0020; private const int WM_LBUTTONDOWN = 0x0201; private const int WM_RBUTTONDOWN = 0x0204; private const int WM_MBUTTONDOWN = 0x0207; private const int WM_XBUTTONDOWN = 0x020B; // back/forward buttons private const int HTCLIENT = 0x1; [DllImport("user32.dll")] private static extern IntPtr DefWindowProc(IntPtr hWnd, int uMsg, IntPtr wParam, IntPtr lParam); private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { switch (msg) { case WM_NCHITTEST: // if the mouse pointer is not over the client area of the tab // ignore it - this disables resize on the glass chrome if (!IsOverClientArea(hwnd, wParam, lParam)) handled = true; break; case WM_SETCURSOR: if (!IsOverClientArea(hwnd, wParam, lParam)) { // the high word of lParam specifies the mouse message identifier // we only want to handle mouse down messages on the border int hiword = (int)lParam >> 16; if (hiword == WM_LBUTTONDOWN || hiword == WM_RBUTTONDOWN || hiword == WM_MBUTTONDOWN || hiword == WM_XBUTTONDOWN) { handled = true; this.Focus(); // focus the window } } break; } return IntPtr.Zero; } private bool IsOverClientArea(IntPtr hwnd, IntPtr wParam, IntPtr LParam) { IntPtr uHitTest = DefWindowProc(hwnd, WM_NCHITTEST, wParam, LParam); if (uHitTest.ToInt32() == HTCLIENT) // check if we're over the client area return true; return false; } #endregion |
With that, your window should now be non-resizable despite still having a border.
Note: since we’re ignoring any window messages when the mouse pointer isn’t over the client area, the application will no longer gain focus when its borders are clicked (it won’t lose focus, either). I’m interested to hear of any solutions to this problem. Update: Code improved to give focus when the border is clicked.
In a future post I will explore positioning windows in relation to notification icons with managed code.
How can we implement it on a form with no title and ControlBox false
This tutorial focuses on WPF, not Windows Forms, but it should be easy to adapt. Just override the Control.WndProc method and use the code in the article (or better yet from the complete sample). You don’t need to worry about the OnSourceInitialized bit, just the WndProc and IsOverClientArea methods.
I was doing the same, just stuck at “handled = true” part.
#region Disable Window Resize
// system constants
private const int WM_NCHITTEST = 0x0084;
private const int WM_SETCURSOR = 0x0020;
private const int WM_LBUTTONDOWN = 0x0201;
private const int WM_RBUTTONDOWN = 0x0204;
private const int WM_MBUTTONDOWN = 0x0207;
private const int WM_XBUTTONDOWN = 0x020B; // back/forward buttons
private const int HTCLIENT = 0x1;
[DllImport("user32.dll")]
private static extern IntPtr DefWindowProc(IntPtr hWnd, int uMsg, IntPtr wParam, IntPtr lParam);
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case WM_NCHITTEST:
// if the mouse pointer is not over the client area of the tab
// ignore it - this disables resize on the glass chrome
if (!IsOverClientArea(m.HWnd, m.WParam, m.LParam))
{
//handled = true;
}
break;
case WM_SETCURSOR:
if (!IsOverClientArea(m.HWnd, m.WParam, m.LParam))
{
// the high word of lParam specifies the mouse message identifier
// we only want to handle mouse down messages on the border
int hiword = (int)m.LParam >> 16;
if (hiword == WM_LBUTTONDOWN
|| hiword == WM_RBUTTONDOWN
|| hiword == WM_MBUTTONDOWN
|| hiword == WM_XBUTTONDOWN)
{
//handled = true;
this.Focus(); // focus the window
}
}
break;
default:
base.WndProc(ref m);
break;
}
}
private bool IsOverClientArea(IntPtr hwnd, IntPtr wParam, IntPtr LParam)
{
IntPtr uHitTest = DefWindowProc(hwnd, WM_NCHITTEST, wParam, LParam);
if (uHitTest.ToInt32() == HTCLIENT) // check if we're over the client area
return true;
return false;
}
#endregion
}
This seems to work:
protected override void WndProc(ref System.Windows.Forms.Message m)
{
bool handled = false;
switch (m.Msg)
{
case WM_NCHITTEST:
// if the mouse pointer is not over the client area of the tab
// ignore it - this disables resize on the glass chrome
if (!IsOverClientArea(m.HWnd, m.WParam, m.LParam))
handled = true;
break;
case WM_SETCURSOR:
if (!IsOverClientArea(m.HWnd, m.WParam, m.LParam))
{
// the high word of lParam specifies the mouse message identifier
// we only want to handle mouse down messages on the border
int hiword = (int)m.LParam >> 16;
if (hiword == WM_LBUTTONDOWN
|| hiword == WM_RBUTTONDOWN
|| hiword == WM_MBUTTONDOWN
|| hiword == WM_XBUTTONDOWN)
{
handled = true;
this.Focus(); // focus the window
}
}
break;
}
if (!handled)
base.WndProc(ref m);
}
obviously it do. thanks