Windows 7-style Notification Area Applications in WPF: Part 3 (Taskbar Position)

View source on GitHub.

In the previous post in this series, I showed how to find the location of a notify icon by implementing the new Windows 7 Shell32.dll function Shell_NotifyIconGetRect in managed code for use with the System.Windows.Forms.NotifyIcon class.

In this post, I will look at how to accurately position a window above (or adjacent to) a notify icon, no matter the location of the taskbar (barring a certain issue that I note at the end of this post :)). As I noted in Part 2, the current version of Keiki is hardcoded to appear in the bottom right-hand corner of the user’s screen, making it very out of place when the taskbar is moved to the top/right/left of the screen. We’ll need to delve into the Win32 API once again, but as in Part 2 the amount of code required is not daunting, even for a Win32 beginner like me.

Possible positions

Basically, we are seeking to determine which edge the taskbar is placed along, and choose our window positioning logic accordingly:

  1. If the taskbar is docked to the bottom of the screen (default), place the window horizontally centred above the notify icon.
  2. If the taskbar is docked to the top of the screen, place the window horizontally centred below the notify icon.
  3. If the taskbar is docked to the left of the screen, place the window vertically centred to the right of the notify icon.
  4. If the taskbar is docked to the right of the screen, place the window vertically centred to the left of the notify icon.

The fly-out containing hidden notify icons in Windows 7 must also be considered, since there is a special case when the taskbar is docked to the left or right of the screen: if the notify icon is contained in the fly-out box, the window should be placed above the icon, not beside it. (This seems like a bit of an arbitrary design decision to me, but it is how the system applications behave.)

Notification Area Right Aligned

It may be possible to determine the location of the taskbar with managed code alone: we could probably use the System.Windows.Forms.Screen class and find the difference between the PrimaryScreen’s Bounds and WorkingArea properties (both Rectangles). I suspect this is a valid approach, but I chose to use the Win32 API function SHAppBarMessage, instead, as it provides all the information we need quite neatly, without any manual calculation required.

SHAppBarMessage

SHAppBarMessage is a function in Shell32.dll which ‘sends an appbar message to the system’. MSDN defines an appbar (‘Application Desktop Toolbar’) like this:

An application desktop toolbar (also called an appbar) is a window that is similar to the Windows taskbar. It is anchored to an edge of the screen, and it typically contains buttons that give the user quick access to other applications and windows. The system prevents other applications from using the desktop area used by an appbar. Any number of appbars can exist on the desktop at any given time.

The Windows taskbar turns out to be a special appbar, itself. (Incidentally, I’m not sure I’ve ever seen a program use this functionality.)

The SHAppBarMessage syntax is:

The dwMessage parameter contains the appbar message to send. We’re interested in using ABM_GETTASKBARPOS (0x00000005), which retrieves the bounding rectangle of the Windows taskbar. It also tells us which edge the taskbar is docked to.

Happily, Crow’s Programming Blog has a great post about using this function in managed code. The code presented is exactly what we need.

Firstly, we need to define the structures and method signature required for SHAppBarMessage (remember that we used RECT in Part 2, so there is no need to define it twice if you’re already using that code):

We need to specify the handle of the taskbar in order to use the ABM_GETTASKBARPOS message. We’ll need another PInvoke signature to find this:

The FindWindow function lets us retrieve the handle of the top-level window whose class name and window name match the specified strings. The class name ‘Shell_TrayWnd’ refers to the taskbar, though (as Crow notes) this doesn’t appear to be officially documented. Update: it seems that just passing in IntPtr.Zero as the handle works just as well, so we might not need FindWindow at all.

Anyway, we can put all this together with a few lines of code:

After calling the function you can get the bounds of the taskbar by looking at the rc property of abdata.

Windows 7 fly-out box

As mentioned at the start of this post, when the task bar is docked to the right or left of the screen and the notify icon is displayed in the Windows 7 fly-out notification area box, the window should be displayed above the icon. We can easily determine whether the notify icon is in the fly-out or not using the result from Shell_NotifyIconGetRect:

inFlyOut will be true if there is no overlap between the rectangles (that is, the notify icon is not within the bounds of the taskbar), and false if there is overlap (the notify icon is within the taskbar’s bounds).

Putting it all together

We now have enough information to accurately position our window next to our notify icon.

Theming & offsets

Though it is not something that most users would notice (or care about if they did :)), the positioning of notification area windows changed slightly from Vista to 7. In Vista (and pre-release Windows 7, actually), the window appeared 1 pixel away from the taskbar/window edge, while in 7, there is an offset of 8 pixels (at 96 DPI; this increases by the DPI factor: it’s 10 pixels at 120 DPI, 12 pixels at 144 DPI, 16 pixels at 192 DPI) from the taskbar/window edge.

DWM Differences

To complicate matters, there is no offset under Aero Basic (that is, when the DWM is disabled). Also, the normal window border is disabled if the DWM is disabled: it’s replaced with a single pixel border whose colour is equal to the SystemColors.WindowFrameColor colour (#646464 in Aero Basic and #000000 in Classic, if you’re curious). Replacing the border is straightforward, so I won’t deal it here. I also won’t specify how to determine the current theme; I’ll just assume that information is accessible.

The code

I will aim to publish a full project later demonstrating this functionality with an empty application. Until then, I’m afraid you’ll have to link all these scattered code fragments together somehow and figure out the missing bits. I hope to release Keiki’s source at some point, too.

A challenger appears

In my testing of the above code, I’ve noticed one problem: the System.Windows.SystemParameters.WorkArea property (which is what my ‘GetWorkingArea()’ function calls on line 27 above) returns the working area of the primary monitor, and there is no guarantee that the taskbar is located on that monitor. If the taskbar is on a different monitor, the above code will behave strangely and end up placing the window on the primary monitor, despite the taskbar being somewhere else.

You may see this as a relatively minor issue, but we have been very pedantic about getting everything exactly right up to here, so it would be poor form to give up at the last hurdle 🙂

I believe that either some more unmanaged Win32 or System.Windows.Forms code will be necessary to work this one out (please correct me if I’m wrong). With WinForms we could probably use the System.Windows.Forms.Screens property to loop over each screen to see which contains the taskbar’s bounding rectangle. Win32 has the MonitorFromRect function which can give us the handle of the monitor that has the largest area of intersection with a given rectangle (the taskbar rectangle, in our case). We can then pass that handle to GetMonitorInfo, which will give us the work area that we should be using to position the window. I will try to cover the Win32 approach in a later post.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *