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 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 public partial class TaskbarIcon : FrameworkElement, IDisposable
45 {
46 #region Members
47
48 private NotifyIconData iconData;
52
53 private readonly WindowMessageSink messageSink;
57
58 private Action singleClickTimerAction;
63
64 private readonly Timer singleClickTimer;
69
70 private readonly Timer balloonCloseTimer;
74
75 public bool IsTaskbarIconCreated { get; private set; }
79
80 public bool SupportsCustomToolTips
86 {
87 get { return messageSink.Version == NotifyIconVersion.Vista; }
88 }
89
90
91 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 public TaskbarIcon()
119 {
120
121 messageSink = Util.IsDesignMode
122 ? WindowMessageSink.CreateEmpty()
123 : new WindowMessageSink(NotifyIconVersion.Win95);
124
125
126 iconData = NotifyIconData.CreateDefault(messageSink.MessageWindowHandle);
127
128
129 CreateTaskbarIcon();
130
131
132 messageSink.MouseEventReceived += OnMouseEvent;
133 messageSink.TaskbarCreated += OnTaskbarCreated;
134 messageSink.ChangeToolTipStateRequest += OnToolTipChange;
135 messageSink.BalloonToolTipChanged += OnBalloonToolTipChanged;
136
137
138 singleClickTimer = new Timer(DoSingleClickAction);
139 balloonCloseTimer = new Timer(CloseBalloonCallback);
140
141
142 if (Application.Current != null) Application.Current.Exit += OnExit;
143 }
144
145 #endregion
146
147 #region Custom Balloons
148
149 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
180 lock (this)
181 {
182 CloseBalloon();
183 }
184
185
186 Popup popup = new Popup();
187 popup.AllowsTransparency = true;
188
189
190 UpdateDataContext(popup, null, DataContext);
191
192
193
194 popup.PopupAnimation = animation;
195
196
197
198
199
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
214
215
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
226 lock (this)
227 {
228 SetCustomBalloon(popup);
229 }
230
231
232 SetParentTaskbarIcon(balloon, this);
233
234
235 RaiseBalloonShowingEvent(balloon, this);
236
237
238 popup.IsOpen = true;
239
240 if (timeout.HasValue)
241 {
242
243 balloonCloseTimer.Change(timeout.Value, Timeout.Infinite);
244 }
245 }
246
247
248 public void ResetBalloonCloseTimer()
256 {
257 if (IsDisposed) return;
258
259 lock (this)
260 {
261
262 balloonCloseTimer.Change(Timeout.Infinite, Timeout.Infinite);
263 }
264 }
265
266
267 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
286 balloonCloseTimer.Change(Timeout.Infinite, Timeout.Infinite);
287
288
289 Popup popup = CustomBalloon;
290 if (popup != null)
291 {
292 UIElement element = popup.Child;
293
294
295 RoutedEventArgs eventArgs = RaiseBalloonClosingEvent(element, this);
296 if (!eventArgs.Handled)
297 {
298
299
300
301
302 popup.IsOpen = false;
303
304
305
306 popup.Child = null;
307
308
309 if (element != null) SetParentTaskbarIcon(element, null);
310 }
311
312
313 SetCustomBalloon(null);
314 }
315 }
316 }
317
318
319 private void CloseBalloonCallback(object state)
324 {
325 if (IsDisposed) return;
326
327
328 Action action = CloseBalloon;
329 this.GetDispatcher().Invoke(action);
330 }
331
332 #endregion
333
334 #region Process Incoming Mouse Events
335
336 private void OnMouseEvent(MouseEvent me)
344 {
345 if (IsDisposed) return;
346
347 switch (me)
348 {
349 case MouseEvent.MouseMove:
350 RaiseTrayMouseMoveEvent();
351
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
373 singleClickTimer.Change(Timeout.Infinite, Timeout.Infinite);
374
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
386 Point cursorPosition = new Point();
387 if (messageSink.Version == NotifyIconVersion.Vista)
388 {
389
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
402 if (me.IsMatch(PopupActivation))
403 {
404 if (me == MouseEvent.IconLeftMouseUp)
405 {
406
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
418 ShowTrayPopup(cursorPosition);
419 }
420 }
421
422
423
424 if (me.IsMatch(MenuActivation))
425 {
426 if (me == MouseEvent.IconLeftMouseUp)
427 {
428
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
440 ShowContextMenu(cursorPosition);
441 }
442 }
443
444
445 if (me == MouseEvent.IconLeftMouseUp && !isLeftClickCommandInvoked)
446 {
447
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 private void OnToolTipChange(bool visible)
467 {
468
469 if (TrayToolTipResolved == null) return;
470
471 if (visible)
472 {
473 if (IsPopupOpen)
474 {
475
476 return;
477 }
478
479 var args = RaisePreviewTrayToolTipOpenEvent();
480 if (args.Handled) return;
481
482 TrayToolTipResolved.IsOpen = true;
483
484
485 if (TrayToolTip != null) RaiseToolTipOpenedEvent(TrayToolTip);
486
487
488 RaiseTrayToolTipOpenEvent();
489 }
490 else
491 {
492 var args = RaisePreviewTrayToolTipCloseEvent();
493 if (args.Handled) return;
494
495
496 if (TrayToolTip != null) RaiseToolTipCloseEvent(TrayToolTip);
497
498 TrayToolTipResolved.IsOpen = false;
499
500
501 RaiseTrayToolTipCloseEvent();
502 }
503 }
504
505
506 private void CreateCustomToolTip()
520 {
521
522 ToolTip tt = TrayToolTip as ToolTip;
523
524 if (tt == null && TrayToolTip != null)
525 {
526
527 tt = new ToolTip();
528 tt.Placement = PlacementMode.Mouse;
529
530
531
532
533
534
535
536 tt.HasDropShadow = false;
537 tt.BorderThickness = new Thickness(0);
538 tt.Background = System.Windows.Media.Brushes.Transparent;
539
540
541 tt.StaysOpen = true;
542 tt.Content = TrayToolTip;
543 }
544 else if (tt == null && !String.IsNullOrEmpty(ToolTipText))
545 {
546
547 tt = new ToolTip();
548 tt.Content = ToolTipText;
549 }
550
551
552
553 if (tt != null)
554 {
555 UpdateDataContext(tt, null, DataContext);
556 }
557
558
559 SetTrayToolTipResolved(tt);
560 }
561
562
563 private void WriteToolTipSettings()
568 {
569 const IconDataMembers flags = IconDataMembers.Tip;
570 iconData.ToolTipText = ToolTipText;
571
572 if (messageSink.Version == NotifyIconVersion.Vista)
573 {
574
575
576 if (String.IsNullOrEmpty(iconData.ToolTipText) && TrayToolTipResolved != null)
577 {
578
579
580 iconData.ToolTipText = "ToolTip";
581 }
582 }
583
584
585 Util.WriteIconData(ref iconData, NotifyCommand.Modify, flags);
586 }
587
588 #endregion
589
590 #region Custom Popup
591
592 private void CreatePopup()
606 {
607
608 Popup popup = TrayPopup as Popup;
609
610 if (popup == null && TrayPopup != null)
611 {
612
613 popup = new Popup();
614 popup.AllowsTransparency = true;
615
616
617
618 popup.PopupAnimation = PopupAnimation.None;
619
620
621
622
623 popup.Child = TrayPopup;
624
625
626
627
628
629
630 popup.Placement = PlacementMode.AbsolutePoint;
631 popup.StaysOpen = false;
632 }
633
634
635
636 if (popup != null)
637 {
638 UpdateDataContext(popup, null, DataContext);
639 }
640
641
642 SetTrayPopupResolved(popup);
643 }
644
645
646 private void ShowTrayPopup(Point cursorPosition)
651 {
652 if (IsDisposed) return;
653
654
655
656 var args = RaisePreviewTrayPopupOpenEvent();
657 if (args.Handled) return;
658
659 if (TrayPopup != null)
660 {
661
662 TrayPopupResolved.Placement = PlacementMode.AbsolutePoint;
663 TrayPopupResolved.HorizontalOffset = cursorPosition.X;
664 TrayPopupResolved.VerticalOffset = cursorPosition.Y;
665
666
667 TrayPopupResolved.IsOpen = true;
668
669 IntPtr handle = IntPtr.Zero;
670 if (TrayPopupResolved.Child != null)
671 {
672
673 HwndSource source = (HwndSource) PresentationSource.FromVisual(TrayPopupResolved.Child);
674 if (source != null) handle = source.Handle;
675 }
676
677
678 if (handle == IntPtr.Zero) handle = messageSink.MessageWindowHandle;
679
680
681
682 WinApi.SetForegroundWindow(handle);
683
684
685
686 if (TrayPopup != null) RaisePopupOpenedEvent(TrayPopup);
687
688
689 RaiseTrayPopupOpenEvent();
690 }
691 }
692
693 #endregion
694
695 #region Context Menu
696
697 private void ShowContextMenu(Point cursorPosition)
702 {
703 if (IsDisposed) return;
704
705
706
707 var args = RaisePreviewTrayContextMenuOpenEvent();
708 if (args.Handled) return;
709
710 if (ContextMenu != null)
711 {
712
713
714
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
723 HwndSource source = (HwndSource) PresentationSource.FromVisual(ContextMenu);
724 if (source != null)
725 {
726 handle = source.Handle;
727 }
728
729
730 if (handle == IntPtr.Zero) handle = messageSink.MessageWindowHandle;
731
732
733
734
735 WinApi.SetForegroundWindow(handle);
736
737
738 RaiseTrayContextMenuOpenEvent();
739 }
740 }
741
742 #endregion
743
744 #region Balloon Tips
745
746 private void OnBalloonToolTipChanged(bool visible)
753 {
754 if (visible)
755 {
756 RaiseTrayBalloonTipShownEvent();
757 }
758 else
759 {
760 RaiseTrayBalloonTipClosedEvent();
761 }
762 }
763
764 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 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 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 public void HideBalloonTip()
826 {
827 EnsureNotDisposed();
828
829
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 private void DoSingleClickAction(object state)
844 {
845 if (IsDisposed) return;
846
847
848 Action action = singleClickTimerAction;
849 if (action != null)
850 {
851
852 singleClickTimerAction = null;
853
854
855 this.GetDispatcher().Invoke(action);
856 }
857 }
858
859 #endregion
860
861 #region Set Version (API)
862
863 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 private void OnTaskbarCreated()
898 {
899 IsTaskbarIconCreated = false;
900 CreateTaskbarIcon();
901 }
902
903
904 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
919 var status = Util.WriteIconData(ref iconData, NotifyCommand.Add, members);
920 if (!status)
921 {
922
923
924
925
926 return;
927 }
928
929
930 SetVersion();
931 messageSink.Version = (NotifyIconVersion) iconData.VersionOrTimeout;
932
933 IsTaskbarIconCreated = true;
934 }
935 }
936 }
937
938 private void RemoveTaskbarIcon()
942 {
943 lock (this)
944 {
945
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 private Point GetDeviceCoordinates(Point point)
964 {
965 if (double.IsNaN(scalingFactor))
966 {
967
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
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 public bool IsDisposed { get; private set; }
992
993
994 private void EnsureNotDisposed()
1000 {
1001 if (IsDisposed) throw new ObjectDisposedException(Name ?? GetType().FullName);
1002 }
1003
1004
1005 private void OnExit(object sender, EventArgs e)
1009 {
1010 Dispose();
1011 }
1012
1013
1014 ~TaskbarIcon()
1024 {
1025 Dispose(false);
1026 }
1027
1028
1029 public void Dispose()
1036 {
1037 Dispose(true);
1038
1039
1040
1041
1042
1043
1044 GC.SuppressFinalize(this);
1045 }
1046
1047
1048 private void Dispose(bool disposing)
1064 {
1065
1066 if (IsDisposed || !disposing) return;
1067
1068 lock (this)
1069 {
1070 IsDisposed = true;
1071
1072
1073 if (Application.Current != null)
1074 {
1075 Application.Current.Exit -= OnExit;
1076 }
1077
1078
1079 singleClickTimer.Dispose();
1080 balloonCloseTimer.Dispose();
1081
1082
1083 messageSink.Dispose();
1084
1085
1086 RemoveTaskbarIcon();
1087 }
1088 }
1089
1090 #endregion
1091 }
1092 }