Skip to content

Commit a4f5aec

Browse files
authored
Trayicon refactoring (#243)
* Refactor tray implementation and introduce TrayIcon class
1 parent f900b46 commit a4f5aec

18 files changed

+829
-438
lines changed

docs/concepts/TrayIcon.md

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,24 @@ var wm = WindowManager.Get(MyWindow);
77
wm.IsVisibleInTray = true;
88
```
99

10-
By default, if the user double-clicks this icon, the window is brought to the front, and if minimized, restored.
10+
By default, if the user clicks/selects this icon, the window is brought to the front, and if minimized, restored.
1111
The icon used is obtained from the Window's task bar icon. You can set the icon using the `Window.AppWindow.SetTaskbarIcon` method.
1212

1313
### Context menu and custom click actions
14-
You can add additional actions to user-inactions by subscribing to the [`TrayIconInvoked`](https://dotmorten.github.io/WinUIEx/api/WinUIEx.WindowManager.TrayIconInvoked.html) event.
15-
This event gives you the ability override the default double-click action by marking the event handled,
16-
or adding any kind of flyout by setting the [`Flyout`](https://dotmorten.github.io/WinUIEx/api/WinUIEx.TrayIconInvokedEventArgs.Flyout.html#WinUIEx_TrayIconInvokedEventArgs_Flyout) property on the event argument.
14+
You can add additional actions to user-inactions by subscribing to the [`TrayIconContextMenu`](https://dotmorten.github.io/WinUIEx/api/WinUIEx.WindowManager.TrayIconContextMenu.html) event.
15+
This event gives you the ability override the display any kind of flyout by setting the
16+
[`Flyout`](https://dotmorten.github.io/WinUIEx/api/WinUIEx.TrayIconEventArgs.Flyout.html#TrayIconEventArgs) property on the event argument.
1717

1818
For example:
1919
```cs
20-
wm.TrayIconInvoked += (w, e) =>
20+
wm.TrayIconContextMenu += (w, e) =>
2121
{
22-
if (e.Type == TrayIconInvokeType.RightMouseUp)
23-
{
24-
var flyout = new MenuFlyout();
25-
flyout.Items.Add(new MenuFlyoutItem() { Text = "Open" });
26-
flyout.Items.Add(new MenuFlyoutItem() { Text = "Quit App" });
27-
((MenuFlyoutItem)flyout.Items[0]).Click += (s, e) => MyWindow.Activate();
28-
((MenuFlyoutItem)flyout.Items[1]).Click += (s, e) => MyWindow.Close();
29-
e.Flyout = flyout;
30-
}
22+
var flyout = new MenuFlyout();
23+
flyout.Items.Add(new MenuFlyoutItem() { Text = "Open" });
24+
flyout.Items.Add(new MenuFlyoutItem() { Text = "Quit App" });
25+
((MenuFlyoutItem)flyout.Items[0]).Click += (s, e) => MyWindow.Activate();
26+
((MenuFlyoutItem)flyout.Items[1]).Click += (s, e) => MyWindow.Close();
27+
e.Flyout = flyout;
3128
};
3229
```
3330

@@ -51,4 +48,66 @@ protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs ar
5148
else
5249
_window.Activate();
5350
}
51+
```
52+
53+
## Using the TrayIcon class
54+
55+
For more fine-grained control over the tray icon, you can use the [`TrayIcon`](https://dotmorten.github.io/WinUIEx/api/WinUIEx.TrayIcon.html) class directly.
56+
This class allows you to create a tray icon without associating it with a window, and gives you more control over its behavior,
57+
updating its icon and tooltip, or even have multiple icons for a single process.
58+
59+
Note: Make sure once you close your application, that all TrayIcon instances are disposed, otherwise the icon will remain in the tray and the process will not exit.
60+
This behavior however also allows you to create a window-less application, or (if needed) creating a Window on demand based on TrayIcon interactions. For example:
61+
62+
```cs
63+
public partial class App : Application
64+
{
65+
private TrayIcon icon;
66+
private Window? _window;
67+
68+
public App()
69+
{
70+
InitializeComponent();
71+
}
72+
73+
private Window GetMainWindow()
74+
{
75+
if (_window is not null)
76+
return _window;
77+
_window = new MainWindow();
78+
_window.AppWindow.Closing += (s, e) =>
79+
{
80+
// Prevent closing so it can be re-activated later. We'll just hide it for now
81+
// As an alternative don't cache the Window, but close and recreate a new Window every time.
82+
e.Cancel = true;
83+
s.Hide();
84+
};
85+
var wm = WindowManager.Get(_window);
86+
wm.WindowStateChanged += (s, state) => wm.AppWindow.IsShownInSwitchers = state != WindowState.Minimized;
87+
return _window;
88+
}
89+
90+
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
91+
{
92+
// In OnLaunched we don't create a window but just the tray icon. We'll create a window later if we need to.
93+
// Note: This icon will keep the application process alive as well.
94+
icon = new TrayIcon(1, "Images/StatusOK.ico", "Test");
95+
icon.IsVisible = true;
96+
icon.Selected += (s, e) => GetMainWindow().Activate();
97+
icon.ContextMenu += (w, e) =>
98+
{
99+
var flyout = new MenuFlyout();
100+
flyout.Items.Add(new MenuFlyoutItem() { Text = "Open" });
101+
((MenuFlyoutItem)flyout.Items[0]).Click += (s, e) => GetMainWindow().Activate();
102+
flyout.Items.Add(new MenuFlyoutItem() { Text = "Quit App" });
103+
((MenuFlyoutItem)flyout.Items[1]).Click += (s, e) =>
104+
{
105+
// Make sure we close both the main window, and the icon for the process to exit
106+
_window?.Close();
107+
icon.Dispose();
108+
};
109+
e.Flyout = flyout;
110+
};
111+
}
112+
}
54113
```

src/WinUIEx/HwndExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ public static void SetWindowSize(IntPtr hwnd, double width, double height)
173173
/// </summary>
174174
/// <param name="hWnd">Window handle</param>
175175
/// <param name="icon">Icon</param>
176+
[Obsolete("Use AppWindow.SetTaskbarIcon")]
176177
public static void SetTaskBarIcon(IntPtr hWnd, Icon? icon)
177178
{
178179
PInvoke.SendMessage(new HWND(hWnd), (uint)Messaging.WindowsMessages.WM_SETICON, new WPARAM(1), new LPARAM(icon?.Handle.Value ?? IntPtr.Zero));

src/WinUIEx/Icon.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ namespace WinUIEx
1717
/// Manages a native Windows Icon instance
1818
/// </summary>
1919
[CreateFromString(MethodName = "WinUIEx.Icon.FromFile")]
20+
[Obsolete]
2021
public unsafe class Icon : IDisposable
2122
{
2223
private readonly HICON handle;

src/WinUIEx/Interop.cs

Lines changed: 48 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ internal static unsafe bool Shell_NotifyIcon(uint dwMessage, in NOTIFYICONDATAW6
124124
[DllImport("Shell32", ExactSpelling = true, EntryPoint = "Shell_NotifyIconW")]
125125
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
126126
internal static extern unsafe bool Shell_NotifyIcon(uint dwMessage, NOTIFYICONDATAW64* lpData);
127+
128+
[DllImport("shell32.dll", SetLastError = true)]
129+
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
130+
internal static extern int Shell_NotifyIconGetRect([In] ref NOTIFYICONIDENTIFIER identifier, [Out] out Windows.Graphics.RectInt32 iconLocation);
127131
}
128132

129133
/// <summary>Contains information about a system appbar message.</summary>
@@ -306,7 +310,7 @@ internal partial struct NOTIFYICONDATAW32
306310
/// <para><see href="https://docs.microsoft.com/windows/win32/api//shellapi/ns-shellapi-notifyicondataw#members">Read more on docs.microsoft.com</see>.</para>
307311
/// </summary>
308312
internal __ushort_256 szInfo;
309-
internal _Anonymous_e__Union Anonymous;
313+
internal uint VersionOrTimeout;
310314
/// <summary>
311315
/// <para>Type: <b>TCHAR[64]</b></para>
312316
/// <para><b>Windows 2000 and later</b>. A null-terminated string that specifies a title for a balloon notification. This title appears in a larger font immediately above the text. It can have a maximum of 64 characters, including the terminating null character, but should be restricted to 48 characters in English to accommodate localization.</para>
@@ -333,36 +337,26 @@ internal partial struct NOTIFYICONDATAW32
333337
/// </summary>
334338
internal HICON hBalloonIcon;
335339

336-
337-
internal struct __ushort_256
338-
{
339-
internal ushort _0, _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, _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, _51, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, _63, _64, _65, _66, _67, _68, _69, _70, _71, _72, _73, _74, _75, _76, _77, _78, _79, _80, _81, _82, _83, _84, _85, _86, _87, _88, _89, _90, _91, _92, _93, _94, _95, _96, _97, _98, _99, _100, _101, _102, _103, _104, _105, _106, _107, _108, _109, _110, _111, _112, _113, _114, _115, _116, _117, _118, _119, _120, _121, _122, _123, _124, _125, _126, _127, _128, _129, _130, _131, _132, _133, _134, _135, _136, _137, _138, _139, _140, _141, _142, _143, _144, _145, _146, _147, _148, _149, _150, _151, _152, _153, _154, _155, _156, _157, _158, _159, _160, _161, _162, _163, _164, _165, _166, _167, _168, _169, _170, _171, _172, _173, _174, _175, _176, _177, _178, _179, _180, _181, _182, _183, _184, _185, _186, _187, _188, _189, _190, _191, _192, _193, _194, _195, _196, _197, _198, _199, _200, _201, _202, _203, _204, _205, _206, _207, _208, _209, _210, _211, _212, _213, _214, _215, _216, _217, _218, _219, _220, _221, _222, _223, _224, _225, _226, _227, _228, _229, _230, _231, _232, _233, _234, _235, _236, _237, _238, _239, _240, _241, _242, _243, _244, _245, _246, _247, _248, _249, _250, _251, _252, _253, _254, _255;
340-
/// <summary>Always <c>256</c>.</summary>
341-
internal int Length => 256;
342-
/// <summary>
343-
/// Gets a ref to an individual element of the inline array.
344-
/// ⚠ Important ⚠: When this struct is on the stack, do not let the returned reference outlive the stack frame that defines it.
345-
/// </summary>
346-
internal ref ushort this[int index] => ref AsSpan()[index];
347-
/// <summary>
348-
/// Gets this inline array as a span.
349-
/// </summary>
350-
/// <remarks>
351-
/// ⚠ Important ⚠: When this struct is on the stack, do not let the returned span outlive the stack frame that defines it.
352-
/// </remarks>
353-
internal Span<ushort> AsSpan() => MemoryMarshal.CreateSpan(ref _0, 256);
354-
}
355-
356-
[StructLayout(LayoutKind.Explicit, Pack = 1)]
357-
internal partial struct _Anonymous_e__Union
358-
{
359-
[FieldOffset(0)]
360-
internal uint uTimeout;
361-
[FieldOffset(0)]
362-
internal uint uVersion;
363-
}
364340
}
365341

342+
internal struct __ushort_256
343+
{
344+
internal ushort _0, _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, _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, _51, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, _63, _64, _65, _66, _67, _68, _69, _70, _71, _72, _73, _74, _75, _76, _77, _78, _79, _80, _81, _82, _83, _84, _85, _86, _87, _88, _89, _90, _91, _92, _93, _94, _95, _96, _97, _98, _99, _100, _101, _102, _103, _104, _105, _106, _107, _108, _109, _110, _111, _112, _113, _114, _115, _116, _117, _118, _119, _120, _121, _122, _123, _124, _125, _126, _127, _128, _129, _130, _131, _132, _133, _134, _135, _136, _137, _138, _139, _140, _141, _142, _143, _144, _145, _146, _147, _148, _149, _150, _151, _152, _153, _154, _155, _156, _157, _158, _159, _160, _161, _162, _163, _164, _165, _166, _167, _168, _169, _170, _171, _172, _173, _174, _175, _176, _177, _178, _179, _180, _181, _182, _183, _184, _185, _186, _187, _188, _189, _190, _191, _192, _193, _194, _195, _196, _197, _198, _199, _200, _201, _202, _203, _204, _205, _206, _207, _208, _209, _210, _211, _212, _213, _214, _215, _216, _217, _218, _219, _220, _221, _222, _223, _224, _225, _226, _227, _228, _229, _230, _231, _232, _233, _234, _235, _236, _237, _238, _239, _240, _241, _242, _243, _244, _245, _246, _247, _248, _249, _250, _251, _252, _253, _254, _255;
345+
/// <summary>Always <c>256</c>.</summary>
346+
internal int Length => 256;
347+
/// <summary>
348+
/// Gets a ref to an individual element of the inline array.
349+
/// ⚠ Important ⚠: When this struct is on the stack, do not let the returned reference outlive the stack frame that defines it.
350+
/// </summary>
351+
internal ref ushort this[int index] => ref AsSpan()[index];
352+
/// <summary>
353+
/// Gets this inline array as a span.
354+
/// </summary>
355+
/// <remarks>
356+
/// ⚠ Important ⚠: When this struct is on the stack, do not let the returned span outlive the stack frame that defines it.
357+
/// </remarks>
358+
internal Span<ushort> AsSpan() => MemoryMarshal.CreateSpan(ref _0, 256);
359+
}
366360

367361
internal struct __ushort_64
368362
{
@@ -454,7 +448,7 @@ internal partial struct NOTIFYICONDATAW64
454448
/// <para><see href="https://docs.microsoft.com/windows/win32/api//shellapi/ns-shellapi-notifyicondataw#members">Read more on docs.microsoft.com</see>.</para>
455449
/// </summary>
456450
internal __ushort_256 szInfo;
457-
internal _Anonymous_e__Union Anonymous;
451+
internal uint VersionOrTimeout;
458452
/// <summary>
459453
/// <para>Type: <b>TCHAR[64]</b></para>
460454
/// <para><b>Windows 2000 and later</b>. A null-terminated string that specifies a title for a balloon notification. This title appears in a larger font immediately above the text. It can have a maximum of 64 characters, including the terminating null character, but should be restricted to 48 characters in English to accommodate localization.</para>
@@ -480,33 +474,30 @@ internal partial struct NOTIFYICONDATAW64
480474
/// <para><see href="https://docs.microsoft.com/windows/win32/api//shellapi/ns-shellapi-notifyicondataw#members">Read more on docs.microsoft.com</see>.</para>
481475
/// </summary>
482476
internal HICON hBalloonIcon;
483-
484-
internal struct __ushort_256
485-
{
486-
internal ushort _0, _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, _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, _51, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, _63, _64, _65, _66, _67, _68, _69, _70, _71, _72, _73, _74, _75, _76, _77, _78, _79, _80, _81, _82, _83, _84, _85, _86, _87, _88, _89, _90, _91, _92, _93, _94, _95, _96, _97, _98, _99, _100, _101, _102, _103, _104, _105, _106, _107, _108, _109, _110, _111, _112, _113, _114, _115, _116, _117, _118, _119, _120, _121, _122, _123, _124, _125, _126, _127, _128, _129, _130, _131, _132, _133, _134, _135, _136, _137, _138, _139, _140, _141, _142, _143, _144, _145, _146, _147, _148, _149, _150, _151, _152, _153, _154, _155, _156, _157, _158, _159, _160, _161, _162, _163, _164, _165, _166, _167, _168, _169, _170, _171, _172, _173, _174, _175, _176, _177, _178, _179, _180, _181, _182, _183, _184, _185, _186, _187, _188, _189, _190, _191, _192, _193, _194, _195, _196, _197, _198, _199, _200, _201, _202, _203, _204, _205, _206, _207, _208, _209, _210, _211, _212, _213, _214, _215, _216, _217, _218, _219, _220, _221, _222, _223, _224, _225, _226, _227, _228, _229, _230, _231, _232, _233, _234, _235, _236, _237, _238, _239, _240, _241, _242, _243, _244, _245, _246, _247, _248, _249, _250, _251, _252, _253, _254, _255;
487-
/// <summary>Always <c>256</c>.</summary>
488-
internal int Length => 256;
489-
/// <summary>
490-
/// Gets a ref to an individual element of the inline array.
491-
/// ⚠ Important ⚠: When this struct is on the stack, do not let the returned reference outlive the stack frame that defines it.
492-
/// </summary>
493-
internal ref ushort this[int index] => ref AsSpan()[index];
494-
/// <summary>
495-
/// Gets this inline array as a span.
496-
/// </summary>
497-
/// <remarks>
498-
/// ⚠ Important ⚠: When this struct is on the stack, do not let the returned span outlive the stack frame that defines it.
499-
/// </remarks>
500-
internal Span<ushort> AsSpan() => MemoryMarshal.CreateSpan(ref _0, 256);
501-
}
477+
}
502478

503-
[StructLayout(LayoutKind.Explicit)]
504-
internal partial struct _Anonymous_e__Union
505-
{
506-
[FieldOffset(0)]
507-
internal uint uTimeout;
508-
[FieldOffset(0)]
509-
internal uint uVersion;
510-
}
479+
[StructLayout(LayoutKind.Sequential)]
480+
internal struct NOTIFYICONIDENTIFIER
481+
{
482+
public uint cbSize;
483+
public nint hWnd;
484+
public uint uID;
485+
public Guid guidItem;
486+
}
487+
488+
internal struct MINMAXINFO
489+
{
490+
#pragma warning disable CS0649
491+
public POINT ptReserved;
492+
public POINT ptMaxSize;
493+
public POINT ptMaxPosition;
494+
public POINT ptMinTrackSize;
495+
public POINT ptMaxTrackSize;
496+
#pragma warning restore CS0649
497+
}
498+
internal struct POINT
499+
{
500+
public int X;
501+
public int Y;
511502
}
512503
}

src/WinUIEx/Messaging/Message.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public override string ToString()
6868
default:
6969
break;
7070
}
71-
return $"{(WindowsMessages)MessageId}: LParam={LParam} WParam={WParam}";
71+
return $"{(WindowsMessages)MessageId}: LParam=0x{LParam.ToString("x")} WParam=0x{WParam.ToString("x")}";
7272
}
7373
}
7474
}

0 commit comments

Comments
 (0)