Table of Contents
- Running the Demo
- Using the Code
- Points of Interest
- Known Issues
I wanted to produce a WPF application that "sits" in the Windows system tray and displays a popup window that fades in and out when the mouse moves over the taskbar icon. In addition, I wanted the user experience to be slick and smooth. I had seen the Live Mesh taskbar client and really liked the way it worked and I thought something similar would be perfect for the application I had in mind.
I have shared this sample to give others a head start on projects with similar requirements.
Producing this popup window proved more difficult than expected as there was a lot of fiddly work involved. The look and feel did not quite display what I wanted and the window animations were initially very clunky. In hindsight, I would have sketched the design and a storyboard out on paper or in Visio before diving in. When using the designers in WPF, XAML can bloat rapidly and one really needs to be careful how you compose your UI elements and animations in order to retain maintainability and flexibility.
A number of unexpected challenges also surfaced and initially the overall goal of a slick and smooth interface seemed very far away. After an initial investigation and some exploratory code, I found that:
- WPF does not natively support a taskbar / system tray icon and the Windows Forms implementation would have to be leveraged
- The standard
NotifyIcon control does not support a mouse leave event
- Animating a WPF window to fade in and out seamlessly and handle interruptions proved difficult
- .NET Framework 3.0 or higher
- Windows XP, 2003, Vista, 2008
Extract the demo from the archive and run WpfXamlPopup.exe. The following diagram and corresponding points will walk you through the features and user interface:
||Moving the mouse over the taskbar icon will result in the popup window displaying.
||Moving the mouse off of the taskbar icon will delay a fraction of a second and then fade the popup window out of view. The short delay allows the mouse to seamlessly move over the desktop and over the popup window. This allows time for the fade out to be cancelled and the user can interact further with the popup window.
||The "PIN" button pins the popup window open regardless of where the mouse pointer is on the desktop.
||The "CLOSE" button unloads the popup window and taskbar icon and closes the application.
||Sample buttons that could be used in an application - I wanted to create an effect where the buttons appear to pop in to focus.
||This radio button group provides a demonstration of how easily the taskbar icon can be changed.
When opening the solution files (source code) for the first time, build the solution so that the project references resolve correctly and the popup window can be viewed in the designer. The code is verbosely commented, so I will not go into much detail, however, I will explain the general architecture and project structure to ramp up your understanding of the solution.
The solution supplied,
WPF XAML Notify Icon and Popup.sln, contains two projects:
ExtendedWindowsControls: A class library that exposes the Windows Forms
NofityIcon control and decorates it with additional methods to cater to the interaction required by this project. It would have been ideal to inherit form the Windows Forms
NotifyIcon class, but unfortunately it is a sealed class, which prevents inheritance.
WpfXamlPopup: The primary project and should be set to the start-up project if you wish to run the application directly from Visual Studio. The project houses the popup window, fade in and out animations and visual style resources.
I will discuss the code file by file, numbered per project below.
ExtendedWindowsControls - This project provides a container for extensions to Windows Forms controls.
MouseLeave event is the crux of this class, so I will begin here:
In order to achieve the popup window behaviour I wanted, I had to build functionality on top of a Windows forms
NotifyIcon to add a
MouseLeave event. By default, the
NotifyIcon only has mouse events for:
MouseClick, MouseDoubleClick, MouseDown, MouseMove and MouseUp. To further complicate the problem, I did not want the new
MouseLeave event to fire immediately, but rather allow for a delay before firing. This delay is essential to the Popup Window in the
WpfXamlPopup project as it allows a time buffer for when the mouse moves off of the
NotifyIcon in the taskbar, which enables three important items:
- The user can move off and back over the icon in a short period of time without the popup window closing (e.g. if the user accidentally moves the mouse off and back over the icon; or leaves the icon briefly and then changes his/her mind and returns to the icon.
- Eliminates flicker/flashing when the mouse is hovering on the edge of the icon and sometimes gets a repeating hit/no hit scenario.
- The user can move from the icon to the popup window without the popup window closing, i.e., if the user moves from the icon to the popup in less than the delay time, then the popup window remains open for a seamless transition.
ExtendedNotifyIcon(int millisecondsToDelayMouseLeaveEvent) is the constructor and sets up an instance of a Windows Forms
NotifyIcon control and associates the
MouseMove event used for tracking the mouse position. The constructor also sets up the
delayMouseLeaveEventTimer timer which is used to time the delay between when the mouse leaves/exits from over the icon and the
MouseLeave event firing. A chained constructor sets the default delay at 100 milliseconds.
targetNotifyIcon_MouseMove method intercepts
MouseMove events on the
NotifyIcon and tracks the position of the mouse on the screen. It also initiates the timer to start (
delayMouseLeaveEventTimer.Start). When the timer fires, it calls method
delayMouseLeaveEventTimer_Tick which checks to see if the mouse is still positioned over the
NotifyIcon. If it is, the method ignores any action. If it is not, it will raise the
MouseLeave event. It is important to recognize that this works for 2 main reasons:
- No action is taken if the timer expires and the mouse is still hovering over the
- When the mouse moves over the icon, it constantly resets the timer and tracks the mouse - this means that the timer for the leave event will initiate for the full duration as the mouse exits from over the icon.
StopMouseLeaveEventFromFiring methods provide manual overrides to the calling code for the custom
IDisposable Interface: If the Windows
NotifyIcon is not disposed, it will remain visible after the application terminates and will only disappear once the user moves the mouse over the icon.
WpfXamlPopup - This is the primary project and contains the WPF popup window.
Images folder contains all images used in the project. This includes different coloured orbs for changing the appearance of the icon in the taskbar and button images for the pin and un-pin actions.
||App.xaml: contains references to various re-usable XAML components stored in resource dictionaries. These include: SlickButtonRD.xaml, MainGridStyleRD.xaml and StealthButtonRD.xaml - discussed below.
||HorizontalSeparatorRD.xaml: A simple user control that uses a combination of rectangles to produce a separator line with an "engraved" effect.
||MainGridStyleRD.xaml: is a resource dictionary that holds the style for the main grid. The main grid forms the basis of the popup window and defines:
LinearGradientBrush: to generate the graded background of the popup window.
OuterGlowBitmapEffect: to generate the shadow around the window to make it stand out from the background.
MainGridBorder: A style that sets the rounded corners, border and default background.
||MainNotifyWindow.xaml: is the XAML for the main form. Thanks to the resource dictionary files and user controls, this main window XAML is simple, uncluttered and elegant and easy to comprehend. During this little foray into WPF, I found that XAML files can quickly become cumbersome and unmanageable. If you use Blend, this becomes even more troublesome as it is easy to generate a lot of irrelevant artifacts which are unused / orphaned and these can become tiresome to clean up.
- The two storyboards
<Storyboard x:Key="gridFadeInStoryBoard"> and
<Storyboard x:Key="gridFadeOutStoryBoard"> simply animate the main grid in and out of view to provide a fade in/out effect. These are manipulated in the code behind (see below for more information).
<Grid x:Name="uiGridMain" Margin="10"> is the "frame" for the popup window and defines the areas for the splitter lines, buttons and window content. It is styled using the resource definition
MainGridBorder in MainGridStyleRD.xaml
SlickToggleButtons are defined in SlickButtonRD.xaml and are used in the control box options for pinning and closing the popup.
- A simple
Label provides the popup window title.
StackPanels define the main content area of the window.
- The first houses the radio buttons for switching between icon images in the taskbar
- The second to contain a selection of buttons that could be used for various actions on the popup window.
MainNotifyWindow() is the constructor and sets up the popup application by:
- adding the
NotifyIcon to the taskbar and registering the relevant events to invoke the popup window
- setting the location of the popup window to the bottom right of the screen
- setting the default state of the popup window to hidden by setting the window and the grid's opacity to 0
- caching the required animations for the popup window and wiring up the events to actions when for when the animations complete
SetNotifyIcon(string iconPrefix) changes the physical icon by accepting a prefix and appending
Orb.ico to generate the name of an icon that has been built in to the resource file. e.g. "BlueOrb.Ico" which is then retrieved using the
pack:// URI notation and assigned to the
SetWindowToBottomRightOfScreen() positions the popup window in the bottom right of the screen.
extendedNotifyIcon_OnShowWindow() handles the event fired when the mouse moves over the
NotifyIcon in the taskbar. If there is no animation executing
(uiGridMain.Opacity > 0 && uiGridMain.Opacity < 1), the handler begins the animation storyboard to fade the popup window in to view. If this is the case, the popup window and main grid are presented immediately to give the user instant access to the popup window. This scenario typically occurs when the user unknowingly moves off of the
NotifyIcon, see's the popup window staring to fade out, and then quickly moves focus back over the icon to "recover" the window.
extendedNotifyIcon_OnHideWindow() handles the event fired when the mouse leaves the
NotifyIcon in the taskbar or when the mouse leaves the popup window. If the window is not pinned open, the handler begins the animation storyboard to fade the popup window out of view. As per the
extendedNotifyIcon_OnShowWindow() method above, this handler only animates when the popup window is fully visible in order to avoid resetting the window's opacity to full, effectively restarting the animation, causing a horrible flicker.
uiWindowMainNotification_MouseEnter(...) handles the event fired when the mouse enters the popup window. In this case, the user obviously wants to interact with the window and all "closing/fade out" events are cancelled and the window's main grid set to full opacity.
uiWindowMainNotification_MouseLeave(...) handles the event fired when the mouse leave's the popup window. In this case, the user is assumed to have completed his work with the popup and the instruction is issues to fade the popup window out of view.
gridFadeOutStoryBoard_Completed(...) this event ensures that the window is out of view by setting its
Opacity = 0;
gridFadeInStoryBoard_Completed(...) this event ensures that the window is fully visible after it has faded in by setting it's
Opacity = 1;
PinButton_Click(...) ensures that the right image is shown on the pin button for button's state (checked / unchecked)
colourRadioButton_Click(...) switches the image used for the
NotifyIcon in the taskbar
CloseButton_Click(...) Performs necessary cleanup of the taskbar icon and closes the application
SlickToggleButton control extends the standard toggle button by adding additional properties to it. These properties are used by the style defined in SlickButtonRD.xaml. By extending the
ToggleButton, I have removed the need for multiple styles when requiring some subtle changes to the button, for example, having different background colours and different corner radius profiles.
||SlickButtonRD.xaml: This resource dictionary contains the style for the toggle buttons in the control box and is used for the Close and Pin buttons. I will not go into extensive detail, but these buttons are complex and are constructed through a control template consisting of 3 border elements to create the background, the border style and then the content target for the button content to be presented in. The
Triggers exposed handle the
Checked properties to create visually appealing button controls.
||StealthButtonRD.xaml: This resource dictionary contains the style for the buttons across the bottom of the popup window. The buttons are unobtrusive to look at on the popup window, but when you move the mouse over them they "pop in to life". This is achieved by using a "dormant/normal" style and a "mouse over" style. A storyboard with a
ThicknessAnimation and manipulation of the button margins provides the "popping" effect.
- Overall, I am satisfied with how this little application works. The animations are slick, the taskbar icon is responsive and the user experience has a quality feel to it.
- In the future, I will sketch concepts out on a whiteboard before beginning a WPF or Silverlight project. I found that trying to lay out items on the canvas and then shift them around and change the styling to be time consuming, messy and fiddly. I found it much easier to outline a concept with some other canvas (or paper) and generate a WPF design from this.
- WPF memory consumption with maximised forms can be quite aggressive. That said, the memory manager does a good job of freeing resources when the window is hidden or minimized.
- A slick WPF design with smoothed edges and reliable animation looks great - but comes at a price. I spent far longer putting this together than I expected and I can't help but think a lot of WPF projects will get delayed due to underestimation of user interface requirements.
- Integrating the Windows Forms NotifyIcon with WPF was simple and works effectively.
- I found that the Blend designer really helped me get going in the start, but later it bogged me down when trying to do more technical items and it also produced superfluous code. I effectively started in Blend and then refactored and finished off in Visual Studio as Blend was just proving too messy and I did not feel I had good control over separation of components and the trickier styling work in Blend.
- Memory usage can be high when the window is visible and in use - I have not found a decent way to manage this better.
- Occasionally the form (when pinned open) can lose its topmost position and become obscured by another window. I am not sure if this is my issue or an issue in the .NET Framework. Please let me know if you experience the same issue or have previously seen this.
- V1.0 - Initial release
- V1.1 - Taskbar integration will be shipped in .NET 4 and should allow this solution to lose its WinForms dependency. See the presentation at http://mschannel9.vo.msecnd.net/o9/mix/09/pptx/t39f.pptx or check out the video at MIX http://videos.visitmix.com/MIX09/T39F for some further information on WPF 4.0 features.
- V1.2 (4 August 2010) - I have tested upgrading the project to Visual Studio 2010 and targeting the .NET 4.0 framework (you must set this on the project properties). The upgrade was a quick and simple process and the application still appears to work perfectly on .NET 4. Result!
- V1.3 (04 November 2015) - I have tested, once again, upgrading the project to Visual Studio 2015 and targeting the .Net 4.6 framework. The upgrade was a quick and simple process and the application still appears to work perfectly on .Net 4.6. Result!
I hope you enjoyed this article and sample. Suggestions, questions and comments are very welcome as well as your vote on how you rate the article and code.