C#
  1  // hardcodet.net NotifyIcon for WPF
  2  // Copyright (c) 2009 - 2013 Philipp Sumi
  3  // Contact and Information: http://www.hardcodet.net
  4  //
  5  // This library is free software; you can redistribute it and/or
  6  // modify it under the terms of the Code Project Open License (CPOL);
  7  // either version 1.0 of the License, or (at your option) any later
  8  // version.
  9  // 
 10  // The above copyright notice and this permission notice shall be
 11  // included in all copies or substantial portions of the Software.
 12  // 
 13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 14  // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 15  // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 16  // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 17  // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 18  // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 19  // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 20  // OTHER DEALINGS IN THE SOFTWARE.
 21  //
 22  // THIS COPYRIGHT NOTICE MAY NOT BE REMOVED FROM THIS FILE
 23  
 24  
 25  using System;
 26  using System.Diagnostics;
 27  using System.Drawing;
 28  using System.Threading;
 29  using System.Windows;
 30  using System.Windows.Controls;
 31  using System.Windows.Controls.Primitives;
 32  using System.Windows.Interop;
 33  using System.Windows.Threading;
 34  using Hardcodet.Wpf.TaskbarNotification.Interop;
 35  using Point = Hardcodet.Wpf.TaskbarNotification.Interop.Point;
 36  
 37  
 38  namespace Hardcodet.Wpf.TaskbarNotification
 39  {
 40      /// <summary>
 41      /// A WPF proxy to for a taskbar icon (NotifyIcon) that sits in the system's
 42      /// taskbar notification area ("system tray").
 43      /// </summary>
 44      public partial class TaskbarIcon : FrameworkElement, IDisposable
 45      {
 46          #region Members
 47  
 48          /// <summary>
 49          /// Represents the current icon data.
 50          /// </summary>
 51          private NotifyIconData iconData;
 52  
 53          /// <summary>
 54          /// Receives messages from the taskbar icon.
 55          /// </summary>
 56          private readonly WindowMessageSink messageSink;
 57  
 58          /// <summary>
 59          /// An action that is being invoked if the
 60          /// <see cref="singleClickTimer"/> fires.
 61          /// </summary>
 62          private Action singleClickTimerAction;
 63  
 64          /// <summary>
 65          /// A timer that is used to differentiate between single
 66          /// and double clicks.
 67          /// </summary>
 68          private readonly Timer singleClickTimer;
 69  
 70          /// <summary>
 71          /// A timer that is used to close open balloon tooltips.
 72          /// </summary>
 73          private readonly Timer balloonCloseTimer;
 74  
 75          /// <summary>
 76          /// Indicates whether the taskbar icon has been created or not.
 77          /// </summary>
 78          public bool IsTaskbarIconCreated { get; private set; }
 79  
 80          /// <summary>
 81          /// Indicates whether custom tooltips are supported, which depends
 82          /// on the OS. Windows Vista or higher is required in order to
 83          /// support this feature.
 84          /// </summary>
 85          public bool SupportsCustomToolTips
 86          {
 87              get { return messageSink.Version == NotifyIconVersion.Vista; }
 88          }
 89  
 90  
 91          /// <summary>
 92          /// Checks whether a non-tooltip popup is currently opened.
 93          /// </summary>
 94          private bool IsPopupOpen
 95          {
 96              get
 97              {
 98                  var popup = TrayPopupResolved;
 99                  var menu = ContextMenu;
100                  var balloon = CustomBalloon;
101  
102                  return popup != null && popup.IsOpen ||
103                         menu != null && menu.IsOpen ||
104                         balloon != null && balloon.IsOpen;
105              }
106          }
107  
108          private double scalingFactor = double.NaN;
109  
110          #endregion
111  
112          #region Construction
113  
114          /// <summary>
115          /// Inits the taskbar icon and registers a message listener
116          /// in order to receive events from the taskbar area.
117          /// </summary>
118          public TaskbarIcon()
119          {
120              //using dummy sink in design mode
121              messageSink = Util.IsDesignMode
122                  ? WindowMessageSink.CreateEmpty()
123                  : new WindowMessageSink(NotifyIconVersion.Win95);
124  
125              //init icon data structure
126              iconData = NotifyIconData.CreateDefault(messageSink.MessageWindowHandle);
127  
128              //create the taskbar icon
129              CreateTaskbarIcon();
130  
131              //register event listeners
132              messageSink.MouseEventReceived += OnMouseEvent;
133              messageSink.TaskbarCreated += OnTaskbarCreated;
134              messageSink.ChangeToolTipStateRequest += OnToolTipChange;
135              messageSink.BalloonToolTipChanged += OnBalloonToolTipChanged;
136  
137              //init single click / balloon timers
138              singleClickTimer = new Timer(DoSingleClickAction);
139              balloonCloseTimer = new Timer(CloseBalloonCallback);
140  
141              //register listener in order to get notified when the application closes
142              if (Application.Current != null) Application.Current.Exit += OnExit;
143          }
144  
145          #endregion
146  
147          #region Custom Balloons
148  
149          /// <summary>
150          /// Shows a custom control as a tooltip in the tray location.
151          /// </summary>
152          /// <param name="balloon"></param>
153          /// <param name="animation">An optional animation for the popup.</param>
154          /// <param name="timeout">The time after which the popup is being closed.
155          /// Submit null in order to keep the balloon open inde
156          /// </param>
157          /// <exception cref="ArgumentNullException">If <paramref name="balloon"/>
158          /// is a null reference.</exception>
159          public void ShowCustomBalloon(UIElement balloon, PopupAnimation animation, int? timeout)
160          {
161              Dispatcher dispatcher = this.GetDispatcher();
162              if (!dispatcher.CheckAccess())
163              {
164                  var action = new Action(() => ShowCustomBalloon(balloon, animation, timeout));
165                  dispatcher.Invoke(DispatcherPriority.Normal, action);
166                  return;
167              }
168  
169              if (balloon == null) throw new ArgumentNullException("balloon");
170              if (timeout.HasValue && timeout < 500)
171              {
172                  string msg = "Invalid timeout of {0} milliseconds. Timeout must be at least 500 ms";
173                  msg = String.Format(msg, timeout);
174                  throw new ArgumentOutOfRangeException("timeout", msg);
175              }
176  
177              EnsureNotDisposed();
178  
179              //make sure we don't have an open balloon
180              lock (this)
181              {
182                  CloseBalloon();
183              }
184  
185              //create an invisible popup that hosts the UIElement
186              Popup popup = new Popup();
187              popup.AllowsTransparency = true;
188  
189              //provide the popup with the taskbar icon's data context
190              UpdateDataContext(popup, null, DataContext);
191  
192              //don't animate by default - devs can use attached
193              //events or override
194              popup.PopupAnimation = animation;
195  
196              //in case the balloon is cleaned up through routed events, the
197              //control didn't remove the balloon from its parent popup when
198              //if was closed the last time - just make sure it doesn't have
199              //a parent that is a popup
200              var parent = LogicalTreeHelper.GetParent(balloon) as Popup;
201              if (parent != null) parent.Child = null;
202  
203              if (parent != null)
204              {
205                  string msg =
206                      "Cannot display control [{0}] in a new balloon popup - that control already has a parent. You may consider creating new balloons every time you want to show one.";
207                  msg = String.Format(msg, balloon);
208                  throw new InvalidOperationException(msg);
209              }
210  
211              popup.Child = balloon;
212  
213              //don't set the PlacementTarget as it causes the popup to become hidden if the
214              //TaskbarIcon's parent is hidden, too...
215              //popup.PlacementTarget = this;
216  
217              popup.Placement = PlacementMode.AbsolutePoint;
218              popup.StaysOpen = true;
219  
220              Point position = TrayInfo.GetTrayLocation();
221              position = GetDeviceCoordinates(position);
222              popup.HorizontalOffset = position.X - 1;
223              popup.VerticalOffset = position.Y - 1;
224  
225              //store reference
226              lock (this)
227              {
228                  SetCustomBalloon(popup);
229              }
230  
231              //assign this instance as an attached property
232              SetParentTaskbarIcon(balloon, this);
233  
234              //fire attached event
235              RaiseBalloonShowingEvent(balloon, this);
236  
237              //display item
238              popup.IsOpen = true;
239  
240              if (timeout.HasValue)
241              {
242                  //register timer to close the popup
243                  balloonCloseTimer.Change(timeout.Value, Timeout.Infinite);
244              }
245          }
246  
247  
248          /// <summary>
249          /// Resets the closing timeout, which effectively
250          /// keeps a displayed balloon message open until
251          /// it is either closed programmatically through
252          /// <see cref="CloseBalloon"/> or due to a new
253          /// message being displayed.
254          /// </summary>
255          public void ResetBalloonCloseTimer()
256          {
257              if (IsDisposed) return;
258  
259              lock (this)
260              {
261                  //reset timer in any case
262                  balloonCloseTimer.Change(Timeout.Infinite, Timeout.Infinite);
263              }
264          }
265  
266  
267          /// <summary>
268          /// Closes the current <see cref="CustomBalloon"/>, if the
269          /// property is set.
270          /// </summary>
271          public void CloseBalloon()
272          {
273              if (IsDisposed) return;
274  
275              Dispatcher dispatcher = this.GetDispatcher();
276              if (!dispatcher.CheckAccess())
277              {
278                  Action action = CloseBalloon;
279                  dispatcher.Invoke(DispatcherPriority.Normal, action);
280                  return;
281              }
282  
283              lock (this)
284              {
285                  //reset timer in any case
286                  balloonCloseTimer.Change(Timeout.Infinite, Timeout.Infinite);
287  
288                  //reset old popup, if we still have one
289                  Popup popup = CustomBalloon;
290                  if (popup != null)
291                  {
292                      UIElement element = popup.Child;
293  
294                      //announce closing
295                      RoutedEventArgs eventArgs = RaiseBalloonClosingEvent(element, this);
296                      if (!eventArgs.Handled)
297                      {
298                          //if the event was handled, clear the reference to the popup,
299                          //but don't close it - the handling code has to manage this stuff now
300  
301                          //close the popup
302                          popup.IsOpen = false;
303  
304                          //remove the reference of the popup to the balloon in case we want to reuse
305                          //the balloon (then added to a new popup)
306                          popup.Child = null;
307  
308                          //reset attached property
309                          if (element != null) SetParentTaskbarIcon(element, null);
310                      }
311  
312                      //remove custom balloon anyway
313                      SetCustomBalloon(null);
314                  }
315              }
316          }
317  
318  
319          /// <summary>
320          /// Timer-invoke event which closes the currently open balloon and
321          /// resets the <see cref="CustomBalloon"/> dependency property.
322          /// </summary>
323          private void CloseBalloonCallback(object state)
324          {
325              if (IsDisposed) return;
326  
327              //switch to UI thread
328              Action action = CloseBalloon;
329              this.GetDispatcher().Invoke(action);
330          }
331  
332          #endregion
333  
334          #region Process Incoming Mouse Events
335  
336          /// <summary>
337          /// Processes mouse events, which are bubbled
338          /// through the class' routed events, trigger
339          /// certain actions (e.g. show a popup), or
340          /// both.
341          /// </summary>
342          /// <param name="me">Event flag.</param>
343          private void OnMouseEvent(MouseEvent me)
344          {
345              if (IsDisposed) return;
346  
347              switch (me)
348              {
349                  case MouseEvent.MouseMove:
350                      RaiseTrayMouseMoveEvent();
351                      //immediately return - there's nothing left to evaluate
352                      return;
353                  case MouseEvent.IconRightMouseDown:
354                      RaiseTrayRightMouseDownEvent();
355                      break;
356                  case MouseEvent.IconLeftMouseDown:
357                      RaiseTrayLeftMouseDownEvent();
358                      break;
359                  case MouseEvent.IconRightMouseUp:
360                      RaiseTrayRightMouseUpEvent();
361                      break;
362                  case MouseEvent.IconLeftMouseUp:
363                      RaiseTrayLeftMouseUpEvent();
364                      break;
365                  case MouseEvent.IconMiddleMouseDown:
366                      RaiseTrayMiddleMouseDownEvent();
367                      break;
368                  case MouseEvent.IconMiddleMouseUp:
369                      RaiseTrayMiddleMouseUpEvent();
370                      break;
371                  case MouseEvent.IconDoubleClick:
372                      //cancel single click timer
373                      singleClickTimer.Change(Timeout.Infinite, Timeout.Infinite);
374                      //bubble event
375                      RaiseTrayMouseDoubleClickEvent();
376                      break;
377                  case MouseEvent.BalloonToolTipClicked:
378                      RaiseTrayBalloonTipClickedEvent();
379                      break;
380                  default:
381                      throw new ArgumentOutOfRangeException("me", "Missing handler for mouse event flag: " + me);
382              }
383  
384  
385              //get mouse coordinates
386              Point cursorPosition = new Point();
387              if (messageSink.Version == NotifyIconVersion.Vista)
388              {
389                  //physical cursor position is supported for Vista and above
390                  WinApi.GetPhysicalCursorPos(ref cursorPosition);
391              }
392              else
393              {
394                  WinApi.GetCursorPos(ref cursorPosition);
395              }
396  
397              cursorPosition = GetDeviceCoordinates(cursorPosition);
398  
399              bool isLeftClickCommandInvoked = false;
400  
401              //show popup, if requested
402              if (me.IsMatch(PopupActivation))
403              {
404                  if (me == MouseEvent.IconLeftMouseUp)
405                  {
406                      //show popup once we are sure it's not a double click
407                      singleClickTimerAction = () =>
408                      {
409                          LeftClickCommand.ExecuteIfEnabled(LeftClickCommandParameter, LeftClickCommandTarget ?? this);
410                          ShowTrayPopup(cursorPosition);
411                      };
412                      singleClickTimer.Change(WinApi.GetDoubleClickTime(), Timeout.Infinite);
413                      isLeftClickCommandInvoked = true;
414                  }
415                  else
416                  {
417                      //show popup immediately
418                      ShowTrayPopup(cursorPosition);
419                  }
420              }
421  
422  
423              //show context menu, if requested
424              if (me.IsMatch(MenuActivation))
425              {
426                  if (me == MouseEvent.IconLeftMouseUp)
427                  {
428                      //show context menu once we are sure it's not a double click
429                      singleClickTimerAction = () =>
430                      {
431                          LeftClickCommand.ExecuteIfEnabled(LeftClickCommandParameter, LeftClickCommandTarget ?? this);
432                          ShowContextMenu(cursorPosition);
433                      };
434                      singleClickTimer.Change(WinApi.GetDoubleClickTime(), Timeout.Infinite);
435                      isLeftClickCommandInvoked = true;
436                  }
437                  else
438                  {
439                      //show context menu immediately
440                      ShowContextMenu(cursorPosition);
441                  }
442              }
443  
444              //make sure the left click command is invoked on mouse clicks
445              if (me == MouseEvent.IconLeftMouseUp && !isLeftClickCommandInvoked)
446              {
447                  //show context menu once we are sure it's not a double click
448                  singleClickTimerAction =
449                      () =>
450                      {
451                          LeftClickCommand.ExecuteIfEnabled(LeftClickCommandParameter, LeftClickCommandTarget ?? this);
452                      };
453                  singleClickTimer.Change(WinApi.GetDoubleClickTime(), Timeout.Infinite);
454              }
455          }
456  
457          #endregion
458  
459          #region ToolTips
460  
461          /// <summary>
462          /// Displays a custom tooltip, if available. This method is only
463          /// invoked for Windows Vista and above.
464          /// </summary>
465          /// <param name="visible">Whether to show or hide the tooltip.</param>
466          private void OnToolTipChange(bool visible)
467          {
468              //if we don't have a tooltip, there's nothing to do here...
469              if (TrayToolTipResolved == null) return;
470  
471              if (visible)
472              {
473                  if (IsPopupOpen)
474                  {
475                      //ignore if we are already displaying something down there
476                      return;
477                  }
478  
479                  var args = RaisePreviewTrayToolTipOpenEvent();
480                  if (args.Handled) return;
481  
482                  TrayToolTipResolved.IsOpen = true;
483  
484                  //raise attached event first
485                  if (TrayToolTip != null) RaiseToolTipOpenedEvent(TrayToolTip);
486  
487                  //bubble routed event
488                  RaiseTrayToolTipOpenEvent();
489              }
490              else
491              {
492                  var args = RaisePreviewTrayToolTipCloseEvent();
493                  if (args.Handled) return;
494  
495                  //raise attached event first
496                  if (TrayToolTip != null) RaiseToolTipCloseEvent(TrayToolTip);
497  
498                  TrayToolTipResolved.IsOpen = false;
499  
500                  //bubble event
501                  RaiseTrayToolTipCloseEvent();
502              }
503          }
504  
505  
506          /// <summary>
507          /// Creates a <see cref="ToolTip"/> control that either
508          /// wraps the currently set <see cref="TrayToolTip"/>
509          /// control or the <see cref="ToolTipText"/> string.<br/>
510          /// If <see cref="TrayToolTip"/> itself is already
511          /// a <see cref="ToolTip"/> instance, it will be used directly.
512          /// </summary>
513          /// <remarks>We use a <see cref="ToolTip"/> rather than
514          /// <see cref="Popup"/> because there was no way to prevent a
515          /// popup from causing cyclic open/close commands if it was
516          /// placed under the mouse. ToolTip internally uses a Popup of
517          /// its own, but takes advance of Popup's internal <see cref="UIElement.IsHitTestVisible"/>
518          /// property which prevents this issue.</remarks>
519          private void CreateCustomToolTip()
520          {
521              //check if the item itself is a tooltip
522              ToolTip tt = TrayToolTip as ToolTip;
523  
524              if (tt == null && TrayToolTip != null)
525              {
526                  //create an invisible wrapper tooltip that hosts the UIElement
527                  tt = new ToolTip();
528                  tt.Placement = PlacementMode.Mouse;
529  
530                  //do *not* set the placement target, as it causes the popup to become hidden if the
531                  //TaskbarIcon's parent is hidden, too. At runtime, the parent can be resolved through
532                  //the ParentTaskbarIcon attached dependency property:
533                  //tt.PlacementTarget = this;
534  
535                  //make sure the tooltip is invisible
536                  tt.HasDropShadow = false;
537                  tt.BorderThickness = new Thickness(0);
538                  tt.Background = System.Windows.Media.Brushes.Transparent;
539  
540                  //setting the 
541                  tt.StaysOpen = true;
542                  tt.Content = TrayToolTip;
543              }
544              else if (tt == null && !String.IsNullOrEmpty(ToolTipText))
545              {
546                  //create a simple tooltip for the ToolTipText string
547                  tt = new ToolTip();
548                  tt.Content = ToolTipText;
549              }
550  
551              //the tooltip explicitly gets the DataContext of this instance.
552              //If there is no DataContext, the TaskbarIcon assigns itself
553              if (tt != null)
554              {
555                  UpdateDataContext(tt, null, DataContext);
556              }
557  
558              //store a reference to the used tooltip
559              SetTrayToolTipResolved(tt);
560          }
561  
562  
563          /// <summary>
564          /// Sets tooltip settings for the class depending on defined
565          /// dependency properties and OS support.
566          /// </summary>
567          private void WriteToolTipSettings()
568          {
569              const IconDataMembers flags = IconDataMembers.Tip;
570              iconData.ToolTipText = ToolTipText;
571  
572              if (messageSink.Version == NotifyIconVersion.Vista)
573              {
574                  //we need to set a tooltip text to get tooltip events from the
575                  //taskbar icon
576                  if (String.IsNullOrEmpty(iconData.ToolTipText) && TrayToolTipResolved != null)
577                  {
578                      //if we have not tooltip text but a custom tooltip, we
579                      //need to set a dummy value (we're displaying the ToolTip control, not the string)
580                      iconData.ToolTipText = "ToolTip";
581                  }
582              }
583  
584              //update the tooltip text
585              Util.WriteIconData(ref iconData, NotifyCommand.Modify, flags);
586          }
587  
588          #endregion
589  
590          #region Custom Popup
591  
592          /// <summary>
593          /// Creates a <see cref="ToolTip"/> control that either
594          /// wraps the currently set <see cref="TrayToolTip"/>
595          /// control or the <see cref="ToolTipText"/> string.<br/>
596          /// If <see cref="TrayToolTip"/> itself is already
597          /// a <see cref="ToolTip"/> instance, it will be used directly.
598          /// </summary>
599          /// <remarks>We use a <see cref="ToolTip"/> rather than
600          /// <see cref="Popup"/> because there was no way to prevent a
601          /// popup from causing cyclic open/close commands if it was
602          /// placed under the mouse. ToolTip internally uses a Popup of
603          /// its own, but takes advance of Popup's internal <see cref="UIElement.IsHitTestVisible"/>
604          /// property which prevents this issue.</remarks>
605          private void CreatePopup()
606          {
607              //check if the item itself is a popup
608              Popup popup = TrayPopup as Popup;
609  
610              if (popup == null && TrayPopup != null)
611              {
612                  //create an invisible popup that hosts the UIElement
613                  popup = new Popup();
614                  popup.AllowsTransparency = true;
615  
616                  //don't animate by default - devs can use attached
617                  //events or override
618                  popup.PopupAnimation = PopupAnimation.None;
619  
620                  //the CreateRootPopup method outputs binding errors in the debug window because
621                  //it tries to bind to "Popup-specific" properties in case they are provided by the child.
622                  //We don't need that so just assign the control as the child.
623                  popup.Child = TrayPopup;
624  
625                  //do *not* set the placement target, as it causes the popup to become hidden if the
626                  //TaskbarIcon's parent is hidden, too. At runtime, the parent can be resolved through
627                  //the ParentTaskbarIcon attached dependency property:
628                  //popup.PlacementTarget = this;
629  
630                  popup.Placement = PlacementMode.AbsolutePoint;
631                  popup.StaysOpen = false;
632              }
633  
634              //the popup explicitly gets the DataContext of this instance.
635              //If there is no DataContext, the TaskbarIcon assigns itself
636              if (popup != null)
637              {
638                  UpdateDataContext(popup, null, DataContext);
639              }
640  
641              //store a reference to the used tooltip
642              SetTrayPopupResolved(popup);
643          }
644  
645  
646          /// <summary>
647          /// Displays the <see cref="TrayPopup"/> control if
648          /// it was set.
649          /// </summary>
650          private void ShowTrayPopup(Point cursorPosition)
651          {
652              if (IsDisposed) return;
653  
654              //raise preview event no matter whether popup is currently set
655              //or not (enables client to set it on demand)
656              var args = RaisePreviewTrayPopupOpenEvent();
657              if (args.Handled) return;
658  
659              if (TrayPopup != null)
660              {
661                  //use absolute position, but place the popup centered above the icon
662                  TrayPopupResolved.Placement = PlacementMode.AbsolutePoint;
663                  TrayPopupResolved.HorizontalOffset = cursorPosition.X;
664                  TrayPopupResolved.VerticalOffset = cursorPosition.Y;
665  
666                  //open popup
667                  TrayPopupResolved.IsOpen = true;
668  
669                  IntPtr handle = IntPtr.Zero;
670                  if (TrayPopupResolved.Child != null)
671                  {
672                      //try to get a handle on the popup itself (via its child)
673                      HwndSource source = (HwndSource) PresentationSource.FromVisual(TrayPopupResolved.Child);
674                      if (source != null) handle = source.Handle;
675                  }
676  
677                  //if we don't have a handle for the popup, fall back to the message sink
678                  if (handle == IntPtr.Zero) handle = messageSink.MessageWindowHandle;
679  
680                  //activate either popup or message sink to track deactivation.
681                  //otherwise, the popup does not close if the user clicks somewhere else
682                  WinApi.SetForegroundWindow(handle);
683  
684                  //raise attached event - item should never be null unless developers
685                  //changed the CustomPopup directly...
686                  if (TrayPopup != null) RaisePopupOpenedEvent(TrayPopup);
687  
688                  //bubble routed event
689                  RaiseTrayPopupOpenEvent();
690              }
691          }
692  
693          #endregion
694  
695          #region Context Menu
696  
697          /// <summary>
698          /// Displays the <see cref="ContextMenu"/> if
699          /// it was set.
700          /// </summary>
701          private void ShowContextMenu(Point cursorPosition)
702          {
703              if (IsDisposed) return;
704  
705              //raise preview event no matter whether context menu is currently set
706              //or not (enables client to set it on demand)
707              var args = RaisePreviewTrayContextMenuOpenEvent();
708              if (args.Handled) return;
709  
710              if (ContextMenu != null)
711              {
712                  //use absolute positioning. We need to set the coordinates, or a delayed opening
713                  //(e.g. when left-clicked) opens the context menu at the wrong place if the mouse
714                  //is moved!
715                  ContextMenu.Placement = PlacementMode.AbsolutePoint;
716                  ContextMenu.HorizontalOffset = cursorPosition.X;
717                  ContextMenu.VerticalOffset = cursorPosition.Y;
718                  ContextMenu.IsOpen = true;
719  
720                  IntPtr handle = IntPtr.Zero;
721  
722                  //try to get a handle on the context itself
723                  HwndSource source = (HwndSource) PresentationSource.FromVisual(ContextMenu);
724                  if (source != null)
725                  {
726                      handle = source.Handle;
727                  }
728  
729                  //if we don't have a handle for the popup, fall back to the message sink
730                  if (handle == IntPtr.Zero) handle = messageSink.MessageWindowHandle;
731  
732                  //activate the context menu or the message window to track deactivation - otherwise, the context menu
733                  //does not close if the user clicks somewhere else. With the message window
734                  //fallback, the context menu can't receive keyboard events - should not happen though
735                  WinApi.SetForegroundWindow(handle);
736  
737                  //bubble event
738                  RaiseTrayContextMenuOpenEvent();
739              }
740          }
741  
742          #endregion
743  
744          #region Balloon Tips
745  
746          /// <summary>
747          /// Bubbles events if a balloon ToolTip was displayed
748          /// or removed.
749          /// </summary>
750          /// <param name="visible">Whether the ToolTip was just displayed
751          /// or removed.</param>
752          private void OnBalloonToolTipChanged(bool visible)
753          {
754              if (visible)
755              {
756                  RaiseTrayBalloonTipShownEvent();
757              }
758              else
759              {
760                  RaiseTrayBalloonTipClosedEvent();
761              }
762          }
763  
764          /// <summary>
765          /// Displays a balloon tip with the specified title,
766          /// text, and icon in the taskbar for the specified time period.
767          /// </summary>
768          /// <param name="title">The title to display on the balloon tip.</param>
769          /// <param name="message">The text to display on the balloon tip.</param>
770          /// <param name="symbol">A symbol that indicates the severity.</param>
771          public void ShowBalloonTip(string title, string message, BalloonIcon symbol)
772          {
773              lock (this)
774              {
775                  ShowBalloonTip(title, message, symbol.GetBalloonFlag(), IntPtr.Zero);
776              }
777          }
778  
779  
780          /// <summary>
781          /// Displays a balloon tip with the specified title,
782          /// text, and a custom icon in the taskbar for the specified time period.
783          /// </summary>
784          /// <param name="title">The title to display on the balloon tip.</param>
785          /// <param name="message">The text to display on the balloon tip.</param>
786          /// <param name="customIcon">A custom icon.</param>
787          /// <exception cref="ArgumentNullException">If <paramref name="customIcon"/>
788          /// is a null reference.</exception>
789          public void ShowBalloonTip(string title, string message, Icon customIcon)
790          {
791              if (customIcon == null) throw new ArgumentNullException("customIcon");
792  
793              lock (this)
794              {
795                  ShowBalloonTip(title, message, BalloonFlags.User, customIcon.Handle);
796              }
797          }
798  
799  
800          /// <summary>
801          /// Invokes <see cref="WinApi.Shell_NotifyIcon"/> in order to display
802          /// a given balloon ToolTip.
803          /// </summary>
804          /// <param name="title">The title to display on the balloon tip.</param>
805          /// <param name="message">The text to display on the balloon tip.</param>
806          /// <param name="flags">Indicates what icon to use.</param>
807          /// <param name="balloonIconHandle">A handle to a custom icon, if any, or
808          /// <see cref="IntPtr.Zero"/>.</param>
809          private void ShowBalloonTip(string title, string message, BalloonFlags flags, IntPtr balloonIconHandle)
810          {
811              EnsureNotDisposed();
812  
813              iconData.BalloonText = message ?? String.Empty;
814              iconData.BalloonTitle = title ?? String.Empty;
815  
816              iconData.BalloonFlags = flags;
817              iconData.CustomBalloonIconHandle = balloonIconHandle;
818              Util.WriteIconData(ref iconData, NotifyCommand.Modify, IconDataMembers.Info | IconDataMembers.Icon);
819          }
820  
821  
822          /// <summary>
823          /// Hides a balloon ToolTip, if any is displayed.
824          /// </summary>
825          public void HideBalloonTip()
826          {
827              EnsureNotDisposed();
828  
829              //reset balloon by just setting the info to an empty string
830              iconData.BalloonText = iconData.BalloonTitle = String.Empty;
831              Util.WriteIconData(ref iconData, NotifyCommand.Modify, IconDataMembers.Info);
832          }
833  
834          #endregion
835  
836          #region Single Click Timer event
837  
838          /// <summary>
839          /// Performs a delayed action if the user requested an action
840          /// based on a single click of the left mouse.<br/>
841          /// This method is invoked by the <see cref="singleClickTimer"/>.
842          /// </summary>
843          private void DoSingleClickAction(object state)
844          {
845              if (IsDisposed) return;
846  
847              //run action
848              Action action = singleClickTimerAction;
849              if (action != null)
850              {
851                  //cleanup action
852                  singleClickTimerAction = null;
853  
854                  //switch to UI thread
855                  this.GetDispatcher().Invoke(action);
856              }
857          }
858  
859          #endregion
860  
861          #region Set Version (API)
862  
863          /// <summary>
864          /// Sets the version flag for the <see cref="iconData"/>.
865          /// </summary>
866          private void SetVersion()
867          {
868              iconData.VersionOrTimeout = (uint) NotifyIconVersion.Vista;
869              bool status = WinApi.Shell_NotifyIcon(NotifyCommand.SetVersion, ref iconData);
870  
871              if (!status)
872              {
873                  iconData.VersionOrTimeout = (uint) NotifyIconVersion.Win2000;
874                  status = Util.WriteIconData(ref iconData, NotifyCommand.SetVersion);
875              }
876  
877              if (!status)
878              {
879                  iconData.VersionOrTimeout = (uint) NotifyIconVersion.Win95;
880                  status = Util.WriteIconData(ref iconData, NotifyCommand.SetVersion);
881              }
882  
883              if (!status)
884              {
885                  Debug.Fail("Could not set version");
886              }
887          }
888  
889          #endregion
890  
891          #region Create / Remove Taskbar Icon
892  
893          /// <summary>
894          /// Recreates the taskbar icon if the whole taskbar was
895          /// recreated (e.g. because Explorer was shut down).
896          /// </summary>
897          private void OnTaskbarCreated()
898          {
899              IsTaskbarIconCreated = false;
900              CreateTaskbarIcon();
901          }
902  
903  
904          /// <summary>
905          /// Creates the taskbar icon. This message is invoked during initialization,
906          /// if the taskbar is restarted, and whenever the icon is displayed.
907          /// </summary>
908          private void CreateTaskbarIcon()
909          {
910              lock (this)
911              {
912                  if (!IsTaskbarIconCreated)
913                  {
914                      const IconDataMembers members = IconDataMembers.Message
915                                                      | IconDataMembers.Icon
916                                                      | IconDataMembers.Tip;
917  
918                      //write initial configuration
919                      var status = Util.WriteIconData(ref iconData, NotifyCommand.Add, members);
920                      if (!status)
921                      {
922                          //couldn't create the icon - we can assume this is because explorer is not running (yet!)
923                          //-> try a bit later again rather than throwing an exception. Typically, if the windows
924                          // shell is being loaded later, this method is being reinvoked from OnTaskbarCreated
925                          // (we could also retry after a delay, but that's currently YAGNI)
926                          return;
927                      }
928  
929                      //set to most recent version
930                      SetVersion();
931                      messageSink.Version = (NotifyIconVersion) iconData.VersionOrTimeout;
932  
933                      IsTaskbarIconCreated = true;
934                  }
935              }
936          }
937  
938          /// <summary>
939          /// Closes the taskbar icon if required.
940          /// </summary>
941          private void RemoveTaskbarIcon()
942          {
943              lock (this)
944              {
945                  //make sure we didn't schedule a creation
946  
947                  if (IsTaskbarIconCreated)
948                  {
949                      Util.WriteIconData(ref iconData, NotifyCommand.Delete, IconDataMembers.Message);
950                      IsTaskbarIconCreated = false;
951                  }
952              }
953          }
954  
955          #endregion
956  
957          /// <summary>
958          /// Recalculates OS coordinates in order to support WPFs coordinate
959          /// system if OS scaling (DPIs) is not 100%.
960          /// </summary>
961          /// <param name="point"></param>
962          /// <returns></returns>
963          private Point GetDeviceCoordinates(Point point)
964          {
965              if (double.IsNaN(scalingFactor))
966              {
967                  //calculate scaling factor in order to support non-standard DPIs
968                  var presentationSource = PresentationSource.FromVisual(this);
969                  if (presentationSource == null)
970                  {
971                      scalingFactor = 1;
972                  }
973                  else
974                  {
975                      var transform = presentationSource.CompositionTarget.TransformToDevice;
976                      scalingFactor = 1/transform.M11;
977                  }
978              }
979  
980              //on standard DPI settings, just return the point
981              if (scalingFactor == 1.0) return point;
982  
983              return new Point() {X = (int) (point.X*scalingFactor), Y = (int) (point.Y*scalingFactor)};
984          }
985  
986          #region Dispose / Exit
987  
988          /// <summary>
989          /// Set to true as soon as <c>Dispose</c> has been invoked.
990          /// </summary>
991          public bool IsDisposed { get; private set; }
992  
993  
994          /// <summary>
995          /// Checks if the object has been disposed and
996          /// raises a <see cref="ObjectDisposedException"/> in case
997          /// the <see cref="IsDisposed"/> flag is true.
998          /// </summary>
999          private void EnsureNotDisposed()
1000         {
1001             if (IsDisposed) throw new ObjectDisposedException(Name ?? GetType().FullName);
1002         }
1003 
1004 
1005         /// <summary>
1006         /// Disposes the class if the application exits.
1007         /// </summary>
1008         private void OnExit(object sender, EventArgs e)
1009         {
1010             Dispose();
1011         }
1012 
1013 
1014         /// <summary>
1015         /// This destructor will run only if the <see cref="Dispose()"/>
1016         /// method does not get called. This gives this base class the
1017         /// opportunity to finalize.
1018         /// <para>
1019         /// Important: Do not provide destructors in types derived from
1020         /// this class.
1021         /// </para>
1022         /// </summary>
1023         ~TaskbarIcon()
1024         {
1025             Dispose(false);
1026         }
1027 
1028 
1029         /// <summary>
1030         /// Disposes the object.
1031         /// </summary>
1032         /// <remarks>This method is not virtual by design. Derived classes
1033         /// should override <see cref="Dispose(bool)"/>.
1034         /// </remarks>
1035         public void Dispose()
1036         {
1037             Dispose(true);
1038 
1039             // This object will be cleaned up by the Dispose method.
1040             // Therefore, you should call GC.SupressFinalize to
1041             // take this object off the finalization queue 
1042             // and prevent finalization code for this object
1043             // from executing a second time.
1044             GC.SuppressFinalize(this);
1045         }
1046 
1047 
1048         /// <summary>
1049         /// Closes the tray and releases all resources.
1050         /// </summary>
1051         /// <summary>
1052         /// <c>Dispose(bool disposing)</c> executes in two distinct scenarios.
1053         /// If disposing equals <c>true</c>, the method has been called directly
1054         /// or indirectly by a user's code. Managed and unmanaged resources
1055         /// can be disposed.
1056         /// </summary>
1057         /// <param name="disposing">If disposing equals <c>false</c>, the method
1058         /// has been called by the runtime from inside the finalizer and you
1059         /// should not reference other objects. Only unmanaged resources can
1060         /// be disposed.</param>
1061         /// <remarks>Check the <see cref="IsDisposed"/> property to determine whether
1062         /// the method has already been called.</remarks>
1063         private void Dispose(bool disposing)
1064         {
1065             //don't do anything if the component is already disposed
1066             if (IsDisposed || !disposing) return;
1067 
1068             lock (this)
1069             {
1070                 IsDisposed = true;
1071 
1072                 //deregister application event listener
1073                 if (Application.Current != null)
1074                 {
1075                     Application.Current.Exit -= OnExit;
1076                 }
1077 
1078                 //stop timers
1079                 singleClickTimer.Dispose();
1080                 balloonCloseTimer.Dispose();
1081 
1082                 //dispose message sink
1083                 messageSink.Dispose();
1084 
1085                 //remove icon
1086                 RemoveTaskbarIcon();
1087             }
1088         }
1089 
1090         #endregion
1091     }
1092 }