6

I am developing a feature within a C# Winforms application which has a requirement for determining the width of the Windows "Notification Area" of the Task Bar (specifically when docked in the default location at the bottom of the screen).

To give better context, the program displays small, transient, borderless windows (tool-tip like), and we would like to ensure that the Notification Area "system tray" icons and clock are never covered by one of these windows.

Below is a screen shot (from Windows 10) of the width I would like to obtain, and how it is intended to be used:

Width dimension needed from Notification Area of Task Bar, and explanation of use

Thus far I have explored the .Net framework, as well as researched Win32 API calls, however I could not identify any potential solutions (nor ascertain whether or not this is indeed possible).

Any suggestions would be greatly appreciated. Ideally a solution would be compatible as far back as Windows 7, however this is not an absolute requirement.

Lemonseed
  • 1,644
  • 1
  • 15
  • 29
  • 3
    Why does it need to determine the width? This feels like a XY Problem - https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem . – mjwills Sep 28 '18 at 11:13
  • @mjwills Our solution uses small, transient, borderless windows (think tool-tip like) and in our specific use case I want to always ensure that the Notification Area/system tray is never covered by one of these windows. – Lemonseed Sep 28 '18 at 11:24
  • @mjwills I've updated the question and diagram to clearly illustrate the problem and approach. – Lemonseed Sep 28 '18 at 11:39
  • 1
    There is only one formal way to do anything like this, NotifyIcon.ShowBalloonTip() in .NET. Doing anything else has a very high risk of being quite troublesome, multiplied by user annoyance. You can figure out how to hack it by using the 64-bit version of Spy++, helps you find the class name of that window back. They haven't changed in a while, the future is murky. Do fret over the user's preferred position of the taskbar and ensuring your app is dpiAware. – Hans Passant Sep 28 '18 at 12:11
  • Sounds like you really want Action Center toast notifications, that's Windows 8 and above, though. – IInspectable Sep 28 '18 at 14:41
  • @IInspectable It is actually the program that is running generating these contextual windows. We just want to ensure that we can position them to avoid covering up the Notification Area of the task bar (icons and clock) specifically. – Lemonseed Sep 28 '18 at 15:11
  • @Lemonseed why not put your window above the taskbar instead of over it? You can easily query the current rectangle of the taskbar itself. Not as easy for the notification area. But IInspectacle is right, you should be using standard system notification APIs instead of inventing your own – Remy Lebeau Sep 28 '18 at 15:39
  • @RemyLebeau That is a good suggestion, to not overlap the task bar at all, rather than try to avoid strictly the Notifications Area. (The contextual windows we are positioning are _not_ actually notifications, the user interacts with them, they can change shape, etc.) – Lemonseed Sep 28 '18 at 15:47
  • 1
    @Jimi Methinks this should be an answer, not a comment. It's kinda tough to read in comment format. – Paul Sanders Sep 29 '18 at 16:39
  • @Paul Sanders I've "extended" the comment. If you think there's something to add to it, let me know. – Jimi Sep 29 '18 at 22:03
  • @Jimi Seems pretty comprehensive to me, thanks. It' seems unlikely to me that Microsoft will change thee class names - after all, why should they? – Paul Sanders Sep 29 '18 at 22:44

1 Answers1

7

The position and size of the Shell Tray Window (or TaskBar) and its child windows can be retrieved using GetWindowRect(), passing the Handle of the related window.
The windows Handle is returned by FindWindowEx() (FindWindowExW), using the window Class Name to identify it. (As a note, this function performs a case-insensitive search).

There are some important details to consider when performing this kind of measures, as Hans Passant noted in his comment.

This operation is not something the Framework or the System need to support. The class names might change in the future. It's not a managed task.

Most important, the application must be DPI Aware. If not, the application is subject to virtualization.
This implies that when a non-DPI-Aware API function is used, its measures/results can be virtualized, too.
GetWindowRect(), for example, is not a DPI-Aware function.

From MSDN:
Mixed-Mode DPI Scaling and DPI-aware APIs

Some notes from a SO question:
Getting a DPI aware correct RECT from GetWindowRect

About DPI Awareness, I've written some notes here.
Also, this (classic) answer from Hans Passant:
How to configure an app to run correctly on a machine with a high DPI setting (e.g. 150%)?

From Raymond Chen's blog:
How can I update my WinForms app to behave better at high DPI, or at normal DPI on very large screens?

From MSDN:
High DPI Desktop Application Development on Windows


The Shell Tray Window has class name Shell_TrayWnd. Its position and relative size can be defined by the user. This is the class Windows extent in its default position.

Shell_TrayWnd

The Tray Notification Area is a child window of Shell_TrayWnd. Its class name is TrayNotifyWnd
(Shell_TrayWnd → TrayNotifyWnd)

TrayNotifyWnd

A couple of other child classes:

The Task Bar, class name MSTaskSwWClass:
(Shell_TrayWnd → ReBarWindow32 → MSTaskSwWClass → MSTaskListWClass)

MSTaskSwWClass

The Tray Clock, class name TrayClockWClass:
(Shell_TrayWnd → TrayNotifyWnd → TrayClockWClass)

TrayClockWClass

These class names applies to these system components in both Windows 7 and Windows 10

Some notes about the Naming Convention here:
Raymond Chen on Why do some people call the taskbar the "tray"?


Shell_TrayWnd parent is Desktop, thus we're passing IntPtr.Zero as parent handle.
GetDesktopWindow() can be used instead.

TrayNotifyWnd is a child window of Shell_TrayWnd. We're using its Handle to speed up the search.

using System.Drawing;
using System.Runtime.InteropServices;

//Shell Tray rectangle
IntPtr hWnd = FindWindowByClassName(IntPtr.Zero, "Shell_TrayWnd");
Rectangle shellTrayArea = GetWindowRectangle(hWnd);

//Notification area rectangle
hWnd = FindWindowByClassName(hWnd, "TrayNotifyWnd");
Rectangle trayNotifyArea = GetWindowRectangle(hWnd);

Windows API Declarations:

public struct RECT
{
    public int Left;
    public int Top;
    public int Right;
    public int Bottom;

    public Rectangle ToRectangle() => Rectangle.FromLTRB(Left, Top, Right, Bottom);
}

[SuppressUnmanagedCodeSecurity, SecurityCritical]
internal static class SafeNativeMethods
{
    [DllImport("User32.dll", SetLastError = true)]
    internal static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect);

    [DllImport("User32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    internal static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
}

//Helper methods
[SecuritySafeCritical]
public static IntPtr FindWindowByClassName(IntPtr hwndParent, string className)
{
    return SafeNativeMethods.FindWindowEx(hwndParent, IntPtr.Zero, className, null);
}

[SecuritySafeCritical]
public static Rectangle GetWindowRectangle(IntPtr windowHandle)
{
    RECT rect;
    new UIPermission(UIPermissionWindow.AllWindows).Demand();
    SafeNativeMethods.GetWindowRect(windowHandle, out rect);
    return rect.ToRectangle();
}
Jimi
  • 29,621
  • 8
  • 43
  • 61
  • Thank-you for an incredible yet simple solution. Just tested your code and it does _exactly_ what I was looking for! – Lemonseed Sep 30 '18 at 13:47
  • That helped me a lot. I have one question though, where can I find those class names? I actually need to find the volume icon rectangle on the notification area (which I believe to be a child of either `TrayNotifyWnd` or `Shell_TrayWnd`). Where can I find the volume window class name or do you happen to know it? Thanks in advance. – DC_AC Oct 30 '19 at 12:46
  • @DC_AC That toolbar (User Promoted Notification Area) is child of `SysPager`: `Shell_TrayWnd -> TrayNotifyWnd -> SysPager -> ToolbarWindow32`. There's another class named User Promoted Notification Area, with class name `ToolbarWindow32`, but it's not child of `SysPager`, it's direct child of `TrayNotifyWnd`. You can find these class names using Spy++ (.exe name `spyxx_amd64.exe`). It's installed with Visual Studio. – Jimi Oct 30 '19 at 13:02
  • @DC_AC You should find it in `C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\Common7\Tools` (in a Visual Studio 2017 installation). – Jimi Oct 30 '19 at 13:10