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