-
-
Notifications
You must be signed in to change notification settings - Fork 49
Adding support for Tray context menus #241
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
94b3d9e
TrayIcon Context Menu
dotMorten df8217b
Refactor code and improve sample
dotMorten 9bc5dc1
Update build settings
dotMorten 1d54c34
Delete dead code
dotMorten 9ca14ab
Fix code review feedback
dotMorten 4d1b46c
Update doc and obsolete APIs replaced by WinAppSDK
dotMorten d73474d
Improve menu
dotMorten ed31236
trayicon cleanup
dotMorten 4201271
Handle trayicon updates
dotMorten 3482c39
remove unused assignment
dotMorten 3930818
Ensure handled is, well handled.
dotMorten 09ec8f8
check return result
dotMorten 8857db1
Update doc
dotMorten File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Refactor code and improve sample
- Loading branch information
commit df8217b344ed1f45f65da27a813e0534dc46f702
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,337 @@ | ||
| using Microsoft.UI.Windowing; | ||
| using Microsoft.UI.Xaml; | ||
| using System; | ||
| using System.Runtime.InteropServices; | ||
| using Windows.Storage; | ||
| using WinUIEx.Messaging; | ||
| using Windows.Win32.UI.WindowsAndMessaging; | ||
| using System.Collections.Generic; | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using Windows.Foundation; | ||
| using Microsoft.UI.Xaml.Controls.Primitives; | ||
|
|
||
| namespace WinUIEx | ||
| { | ||
| public partial class WindowManager | ||
| { | ||
| private bool _isVisibleInTray = false; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a value indicating whether the window is shown in the system tray. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// <para>The system tray icon will use the same icon as Window's Taskbar icon, and tooltip will match the AppWindow.Title value. Double-clicking the icon restores the window if minimized and brings it to the front.</para> | ||
| /// <para>See <see cref="WindowExtensions.SetIsShownInSwitchers" /> to hide the window from the Alt+Tab switcher and task bar. | ||
| /// If you want to minimize the window to the tray, set this to <c>true</c> and when <see cref="WindowManager.WindowStateChanged"/> is fired and changes to minimized, | ||
| /// hide it from the switcher.</para> | ||
| /// </remarks> | ||
| /// <seealso cref="TrayIconInvoked"/> | ||
| public bool IsVisibleInTray | ||
| { | ||
| get => _isVisibleInTray; | ||
| set | ||
| { | ||
| if (_isVisibleInTray != value) | ||
| { | ||
| _isVisibleInTray = value; | ||
| if (value) | ||
| AddToTray(); | ||
| else | ||
| RemoveFromTray(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private void AddToTray() | ||
| { | ||
| // See https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shell_notifyicona | ||
| const uint NIM_ADD = 0x00000000; | ||
| // const uint NIM_MODIFY = 0x00000001; | ||
| const uint NIF_MESSAGE = 0x00000001; | ||
| const uint NIF_ICON = 0x00000002; | ||
| const uint NIF_TIP = 0x00000004; | ||
| var hicon = new HICON(currentIcon); | ||
| Windows.Win32.__ushort_128 tip = new Windows.Win32.__ushort_128(); | ||
| for (int i = 0; i < 128 && i < AppWindow.Title.Length; i++) | ||
| { | ||
| tip[i] = (ushort)AppWindow.Title[i]; | ||
| } | ||
|
|
||
| if (Environment.Is64BitProcess) | ||
| { | ||
| var notifyIconData = new Windows.Win32.NOTIFYICONDATAW64 | ||
| { | ||
| hWnd = new Windows.Win32.Foundation.HWND(_window.GetWindowHandle()), | ||
| cbSize = (uint)Marshal.SizeOf<Windows.Win32.NOTIFYICONDATAW64>(), | ||
| uID = 0, | ||
| uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP, // Icon and callback message is set and valid | ||
| hIcon = hicon, | ||
| uCallbackMessage = 0x8765, | ||
| szTip = tip | ||
| }; | ||
| Windows.Win32.PInvoke.Shell_NotifyIcon(NIM_ADD, notifyIconData); | ||
| currentTrayIcon = new NOTIFYICONIDENTIFIER() | ||
| { | ||
| uID = 0, | ||
| hWnd = notifyIconData.hWnd, | ||
| cbSize = (uint)(uint)Marshal.SizeOf<NOTIFYICONIDENTIFIER>(), | ||
| }; | ||
| } | ||
| else | ||
| { | ||
| var notifyIconData = new Windows.Win32.NOTIFYICONDATAW32 | ||
| { | ||
| hWnd = new Windows.Win32.Foundation.HWND(_window.GetWindowHandle()), | ||
| cbSize = (uint)Marshal.SizeOf<Windows.Win32.NOTIFYICONDATAW32>(), | ||
| uID = 0, | ||
| uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP, // Icon and callback message is set and valid | ||
| hIcon = hicon, | ||
| uCallbackMessage = 0x8765, | ||
| szTip = tip | ||
| }; | ||
| Windows.Win32.PInvoke.Shell_NotifyIcon(NIM_ADD, notifyIconData); | ||
| currentTrayIcon = new NOTIFYICONIDENTIFIER() | ||
| { | ||
| uID = 0, | ||
| hWnd = notifyIconData.hWnd, | ||
| cbSize = (uint)(uint)Marshal.SizeOf<NOTIFYICONIDENTIFIER>(), | ||
|
dotMorten marked this conversation as resolved.
Outdated
|
||
| }; | ||
| } | ||
| } | ||
|
|
||
| private void RemoveFromTray() | ||
| { | ||
| const uint NIM_DELETE = 0x00000002; | ||
| if (Environment.Is64BitProcess) | ||
| { | ||
| var notifyIconData = new Windows.Win32.NOTIFYICONDATAW64 | ||
| { | ||
| hWnd = new Windows.Win32.Foundation.HWND(_window.GetWindowHandle()), | ||
| cbSize = (uint)Marshal.SizeOf<Windows.Win32.NOTIFYICONDATAW64>(), | ||
| uID = 0, | ||
| }; | ||
| Windows.Win32.PInvoke.Shell_NotifyIcon(NIM_DELETE, notifyIconData); | ||
| } | ||
| else | ||
| { | ||
| var notifyIconData = new Windows.Win32.NOTIFYICONDATAW32 | ||
| { | ||
| hWnd = new Windows.Win32.Foundation.HWND(_window.GetWindowHandle()), | ||
| cbSize = (uint)Marshal.SizeOf<Windows.Win32.NOTIFYICONDATAW32>(), | ||
| }; | ||
| Windows.Win32.PInvoke.Shell_NotifyIcon(NIM_DELETE, notifyIconData); | ||
| } | ||
| currentTrayIcon = null; | ||
| } | ||
|
|
||
| private void ProcessTrayIconEvents(Message message) | ||
| { | ||
| switch ((WindowsMessages)(message.LParam & 0xffff)) | ||
| { | ||
| case WindowsMessages.WM_LBUTTONDBLCLK: | ||
| HandleTrayIconClick(TrayIconInvokeType.LeftDoubleClick); | ||
| break; | ||
| case WindowsMessages.WM_RBUTTONDBLCLK: | ||
| HandleTrayIconClick(TrayIconInvokeType.RightDoubleClick); | ||
| break; | ||
| case WindowsMessages.WM_RBUTTONUP: | ||
| HandleTrayIconClick(TrayIconInvokeType.RightMouseUp); | ||
| break; | ||
| case WindowsMessages.WM_RBUTTONDOWN: | ||
| HandleTrayIconClick(TrayIconInvokeType.RightMouseDown); | ||
| break; | ||
| case WindowsMessages.WM_LBUTTONUP: | ||
| HandleTrayIconClick(TrayIconInvokeType.LeftMouseUp); | ||
| break; | ||
| case WindowsMessages.WM_LBUTTONDOWN: | ||
| HandleTrayIconClick(TrayIconInvokeType.LeftMouseDown); | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| [StructLayout(LayoutKind.Sequential)] | ||
| private struct NOTIFYICONIDENTIFIER | ||
| { | ||
| public uint cbSize; | ||
| public IntPtr hWnd; | ||
| public Int32 uID; | ||
| public Guid guidItem; | ||
| } | ||
| [DllImport("shell32.dll", SetLastError = true)] | ||
| private static extern int Shell_NotifyIconGetRect([In] ref NOTIFYICONIDENTIFIER identifier, [Out] out Windows.Graphics.RectInt32 iconLocation); | ||
|
dotMorten marked this conversation as resolved.
|
||
| private NOTIFYICONIDENTIFIER? currentTrayIcon; | ||
| private void HandleTrayIconClick(TrayIconInvokeType type) | ||
| { | ||
| bool handled = false; | ||
| if (TrayIconInvoked is EventHandler<TrayIconInvokedEventArgs> handler) | ||
|
dotMorten marked this conversation as resolved.
|
||
| { | ||
| var args = new TrayIconInvokedEventArgs(type); | ||
| TrayIconInvoked.Invoke(this, args); | ||
| if (args.Flyout is FlyoutBase flyout && currentTrayIcon.HasValue) | ||
| { | ||
| var icon = currentTrayIcon.Value; | ||
| Shell_NotifyIconGetRect(ref icon, out var location); | ||
| var w = new TrayIconWindow(flyout); | ||
| w.ShowAt(location.X, location.Y); | ||
| } | ||
| } | ||
| if (!handled && type == TrayIconInvokeType.LeftDoubleClick) | ||
|
dotMorten marked this conversation as resolved.
|
||
| { | ||
| // Default action | ||
| // If icon was double-clicked, restore the window and bring to front | ||
| if (_windowState == WindowState.Minimized) | ||
| { | ||
| WindowExtensions.Restore(_window); | ||
| } | ||
| WindowExtensions.SetForegroundWindow(_window); | ||
| } | ||
| } | ||
|
|
||
| private class TrayIconWindow : Window | ||
| { | ||
| private WindowManager manager; | ||
| private readonly FlyoutBase flyout; | ||
| ~TrayIconWindow() | ||
| { | ||
|
|
||
| } | ||
|
dotMorten marked this conversation as resolved.
Outdated
|
||
| public TrayIconWindow(FlyoutBase flyout) | ||
| { | ||
| manager = WindowManager.Get(this); | ||
| manager.MinHeight = 0; | ||
| manager.MinWidth = 0; | ||
| WindowExtensions.SetWindowStyle(this, WindowStyle.Popup); | ||
| AppWindow.IsShownInSwitchers = false; | ||
|
|
||
| this.Closed += TrayIconWindow_Closed; | ||
| this.Content = new Microsoft.UI.Xaml.Controls.Grid(); | ||
| ((FrameworkElement)this.Content).Loaded += TrayIconWindow_Loaded; | ||
| this.flyout = flyout; | ||
| flyout.Closing += Flyout_Closing; | ||
| manager.WindowMessageReceived += Manager_WindowMessageReceived; | ||
| } | ||
|
|
||
| internal void ShowAt(int x, int y) | ||
| { | ||
| Activate(); | ||
| AppWindow.MoveAndResize(new Windows.Graphics.RectInt32(x, y, 0, 0), Microsoft.UI.Windowing.DisplayArea.GetFromPoint(new Windows.Graphics.PointInt32(0, 0), Microsoft.UI.Windowing.DisplayAreaFallback.Primary)); | ||
|
dotMorten marked this conversation as resolved.
|
||
| WindowExtensions.SetForegroundWindow(this); | ||
| } | ||
|
|
||
| private void Flyout_Closing(FlyoutBase sender, FlyoutBaseClosingEventArgs args) | ||
| { | ||
| Close(); | ||
| } | ||
|
|
||
| private void TrayIconWindow_Loaded(object sender, RoutedEventArgs e) | ||
| { | ||
| flyout.ShouldConstrainToRootBounds = false; | ||
| flyout.ShowAt((FrameworkElement)this.Content, new FlyoutShowOptions() | ||
| { | ||
| ShowMode = FlyoutShowMode.Auto, | ||
| Placement = FlyoutPlacementMode.Auto, | ||
| Position = new Point(0, 0) | ||
| }); | ||
| } | ||
|
|
||
| private void TrayIconWindow_Closed(object sender, WindowEventArgs args) | ||
| { | ||
| manager.WindowMessageReceived -= Manager_WindowMessageReceived; | ||
| ((FrameworkElement)this.Content).Loaded -= TrayIconWindow_Loaded; | ||
| flyout.Closing -= Flyout_Closing; | ||
| Closed -= TrayIconWindow_Closed; | ||
| Content = null; | ||
| } | ||
|
|
||
| private void Manager_WindowMessageReceived(object? sender, WindowMessageEventArgs e) | ||
| { | ||
| if (e.MessageType == WindowsMessages.WM_ACTIVATE) | ||
| { | ||
| if (e.Message.WParam == 0) // Window lost focus | ||
| { | ||
| var result = this.DispatcherQueue.TryEnqueue(() => Hide()); | ||
|
dotMorten marked this conversation as resolved.
Outdated
|
||
| } | ||
| } | ||
| } | ||
| private void Hide() | ||
| { | ||
| flyout.Hide(); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Raised when the user invokes the trayicon by clicking or accessing via keyboard | ||
| /// </summary> | ||
| /// <seealso cref="IsVisibleInTray"/> | ||
| public event EventHandler<TrayIconInvokedEventArgs>? TrayIconInvoked; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// The event arguments for the <see cref="WindowManager.TrayIconInvoked"/> event. | ||
| /// </summary> | ||
| public class TrayIconInvokedEventArgs : EventArgs | ||
| { | ||
| internal TrayIconInvokedEventArgs(TrayIconInvokeType type) | ||
| { | ||
| Type = type; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the way the tray icon was invoked. | ||
| /// </summary> | ||
| public TrayIconInvokeType Type { get; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a flyout to display by the trayicon | ||
| /// </summary> | ||
| public FlyoutBase? Flyout { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Set to true to avoid any default behavior | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// When the type is <see cref="TrayIconInvokeType.LeftDoubleClick"/> | ||
| /// the window is restored and brought to the front. By marking this event | ||
| /// handled, this default behavior will be disabled. | ||
| /// </remarks> | ||
| public bool Handled { get; set; } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Describes the way the tray icon was interacted with | ||
| /// </summary> | ||
| /// <seealso cref="WindowManager.TrayIconInvoked"/> | ||
| /// <seealso cref="WindowManager.IsVisibleInTray"/> | ||
| public enum TrayIconInvokeType | ||
| { | ||
| /// <summary> | ||
| /// User moused down on the tray icon using the left button. | ||
| /// </summary> | ||
| LeftMouseDown, | ||
|
|
||
| /// <summary> | ||
| /// User moused down on the tray icon using the right button. | ||
| /// </summary> | ||
| RightMouseDown, | ||
|
|
||
| /// <summary> | ||
| /// User released the left mouse button on the tray icon. | ||
| /// </summary> | ||
| LeftMouseUp, | ||
|
|
||
| /// <summary> | ||
| /// User released the left mouse button on the tray icon. | ||
|
dotMorten marked this conversation as resolved.
Outdated
|
||
| /// </summary> | ||
| RightMouseUp, | ||
|
|
||
| /// <summary> | ||
| /// User double-clicked the left mouse button on the tray icon. | ||
| /// </summary> | ||
| LeftDoubleClick, | ||
|
|
||
| /// <summary> | ||
| /// User double-clicked the right mouse button on the tray icon. | ||
| /// </summary> | ||
| RightDoubleClick, | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.