Skip to content

Commit 0759e97

Browse files
committed
Added notify icon utility. Removed duplicate WindowProc => WNDPROC
1 parent 3b23307 commit 0759e97

17 files changed

+2495
-18
lines changed

DirectN.Extensions/Utilities/Icon.cs

+6-5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ public class Icon : IDisposable
55
private nint _handle;
66

77
public HICON Handle => new() { Value = _handle };
8+
public virtual bool DestroyHandleOnDispose { get; set; }
89

910
public static Icon? LoadApplicationIcon(int size = 16)
1011
{
@@ -13,15 +14,15 @@ public class Icon : IDisposable
1314
return null;
1415

1516
var exeHandle = Functions.GetModuleHandleW(PWSTR.From(path));
16-
return FromHandle(Functions.LoadImageW(new HINSTANCE { Value = exeHandle.Value }, Constants.IDI_APPLICATION, GDI_IMAGE_TYPE.IMAGE_ICON, size, size, 0).Value);
17+
return FromHandle(Functions.LoadImageW(new HINSTANCE { Value = exeHandle.Value }, Constants.IDI_APPLICATION, GDI_IMAGE_TYPE.IMAGE_ICON, size, size, 0).Value, true);
1718
}
1819

19-
public static Icon? FromHandle(nint handle)
20+
public static Icon? FromHandle(nint handle, bool destroyHandleOnDispose = false)
2021
{
2122
if (handle == 0)
2223
return null;
2324

24-
return new Icon { _handle = handle };
25+
return new Icon { _handle = handle, DestroyHandleOnDispose = destroyHandleOnDispose };
2526
}
2627

2728
public static bool Destroy(ref HICON handle)
@@ -53,7 +54,7 @@ public static bool Destroy(nint handle)
5354
if (handle == 0)
5455
return null;
5556

56-
return new Icon { _handle = handle };
57+
return new Icon { _handle = handle, DestroyHandleOnDispose = true };
5758
}
5859

5960
public static string? PathParseIconLocation(string? path, out int index)
@@ -74,7 +75,7 @@ protected virtual void Dispose(bool disposing)
7475
if (disposing)
7576
{
7677
var handle = Interlocked.Exchange(ref _handle, 0);
77-
if (handle != 0)
78+
if (handle != 0 && DestroyHandleOnDispose)
7879
{
7980
Functions.DestroyIcon(new HICON { Value = handle });
8081
}
+339
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
namespace DirectN.Extensions.Utilities;
2+
3+
public class NotifyIcon : IDisposable
4+
{
5+
protected static WNDPROC DefWindowProc { get; } = Marshal.GetDelegateForFunctionPointer<WNDPROC>(Functions.GetProcAddress(Functions.GetModuleHandleW(PWSTR.From("user32.dll")), PSTR.From("DefWindowProcW")));
6+
7+
private NotifyIconNativeWindow? _window;
8+
private string _text = string.Empty;
9+
private bool _added;
10+
private bool _visible;
11+
private HICON _iconHandle;
12+
13+
public event EventHandler<ValueEventArgs<POINT>>? MenuOpening;
14+
15+
public NotifyIcon()
16+
{
17+
TaskbarCreatedMessage = Functions.RegisterWindowMessageW(PWSTR.From("TaskbarCreated"));
18+
TrayMouseMessage = MessageDecoder.WM_USER + 1024;
19+
_window = new NotifyIconNativeWindow(this);
20+
UpdateIcon(_visible);
21+
}
22+
23+
protected virtual uint TrayMouseMessage { get; }
24+
protected virtual uint TaskbarCreatedMessage { get; }
25+
public HWND WindowHandle => _window?.Handle ?? HWND.Null;
26+
27+
public virtual bool Visible
28+
{
29+
get => _visible;
30+
set
31+
{
32+
if (_visible == value)
33+
return;
34+
35+
UpdateIcon(value);
36+
_visible = value;
37+
}
38+
}
39+
40+
public virtual string Text
41+
{
42+
get => _text;
43+
set
44+
{
45+
if (value == _text)
46+
return;
47+
48+
value ??= string.Empty;
49+
if (value.Length > 128)
50+
{
51+
value = value[..128];
52+
}
53+
54+
_text = value;
55+
if (_added)
56+
{
57+
UpdateIcon(true);
58+
}
59+
}
60+
}
61+
62+
public virtual HICON IconHandle
63+
{
64+
get => _iconHandle;
65+
set
66+
{
67+
if (_iconHandle == value)
68+
return;
69+
70+
_iconHandle = value;
71+
if (_added)
72+
{
73+
UpdateIcon(true);
74+
}
75+
}
76+
}
77+
78+
public void SetFocusToNotificationArea() => SetFocusToNotificationArea((_window?.Handle).GetValueOrDefault());
79+
public unsafe static void SetFocusToNotificationArea(HWND handle)
80+
{
81+
var data = new NOTIFYICONDATAW
82+
{
83+
cbSize = (uint)sizeof(NOTIFYICONDATAW),
84+
hWnd = handle
85+
};
86+
Functions.Shell_NotifyIconW(NOTIFY_ICON_MESSAGE.NIM_SETFOCUS, data);
87+
}
88+
89+
public static void CloseNotificationArea()
90+
{
91+
// note when waiting a bit the notification window goes away automatically
92+
// but this is a hack of some sort... I have not found a way to do it better
93+
var handle = Functions.FindWindowW(PWSTR.From("NotifyIconOverflowWindow"), PWSTR.Null);
94+
if (handle != 0)
95+
{
96+
Functions.SendMessageW(handle, MessageDecoder.WM_CLOSE, 0, 0);
97+
}
98+
else
99+
{
100+
// windows 11
101+
handle = Functions.FindWindowW(PWSTR.From("TopLevelWindowForOverflowXamlIsland"), PWSTR.Null);
102+
if (handle != 0)
103+
{
104+
Functions.ShowWindow(handle, SHOW_WINDOW_CMD.SW_HIDE);
105+
}
106+
}
107+
}
108+
109+
private unsafe void UpdateIcon(bool showIconInTray)
110+
{
111+
var window = _window;
112+
if (window == null)
113+
return;
114+
115+
var data = new NOTIFYICONDATAW
116+
{
117+
cbSize = (uint)sizeof(NOTIFYICONDATAW),
118+
uCallbackMessage = TrayMouseMessage,
119+
uFlags = NOTIFY_ICON_DATA_FLAGS.NIF_MESSAGE | NOTIFY_ICON_DATA_FLAGS.NIF_TIP | NOTIFY_ICON_DATA_FLAGS.NIF_SHOWTIP,
120+
Anonymous = new NOTIFYICONDATAW._Anonymous_e__Union { uVersion = Constants.NOTIFYICON_VERSION_4 },
121+
hWnd = window.Handle
122+
};
123+
124+
if (IconHandle != 0)
125+
{
126+
data.uFlags |= NOTIFY_ICON_DATA_FLAGS.NIF_ICON;
127+
data.hIcon = IconHandle;
128+
}
129+
130+
data.szTip = Text;
131+
132+
if (showIconInTray && IconHandle != 0)
133+
{
134+
if (!_added)
135+
{
136+
if (!Functions.Shell_NotifyIconW(NOTIFY_ICON_MESSAGE.NIM_ADD, data))
137+
throw new Win32Exception(Marshal.GetLastWin32Error());
138+
139+
Functions.Shell_NotifyIconW(NOTIFY_ICON_MESSAGE.NIM_SETVERSION, data);
140+
_added = true;
141+
}
142+
else
143+
{
144+
Functions.Shell_NotifyIconW(NOTIFY_ICON_MESSAGE.NIM_MODIFY, data);
145+
}
146+
}
147+
else if (_added)
148+
{
149+
Functions.Shell_NotifyIconW(NOTIFY_ICON_MESSAGE.NIM_DELETE, data);
150+
_added = false;
151+
}
152+
}
153+
154+
protected virtual void OnMenuOpening(object? sender, ValueEventArgs<POINT> e) => MenuOpening?.Invoke(sender, e);
155+
156+
// if needed, override to handle specific events
157+
protected virtual LRESULT WindowProc(HWND hWnd, uint msg, WPARAM wParam, LPARAM lParam)
158+
{
159+
var x = wParam.Value.SignedLOWORD();
160+
var y = wParam.Value.SignedHIWORD();
161+
162+
if (msg == TrayMouseMessage)
163+
{
164+
var tmsg = (uint)lParam.Value.ToInt64();
165+
166+
switch (tmsg)
167+
{
168+
case MessageDecoder.WM_LBUTTONDBLCLK:
169+
break;
170+
171+
case MessageDecoder.WM_LBUTTONDOWN:
172+
break;
173+
174+
case MessageDecoder.WM_LBUTTONUP:
175+
OnMenuOpening(this, new ValueEventArgs<POINT>(new POINT(x, y)));
176+
CloseNotificationArea();
177+
break;
178+
179+
case MessageDecoder.WM_MBUTTONDBLCLK:
180+
break;
181+
182+
case MessageDecoder.WM_MBUTTONDOWN:
183+
break;
184+
185+
case MessageDecoder.WM_MBUTTONUP:
186+
break;
187+
188+
case MessageDecoder.WM_MOUSEMOVE:
189+
break;
190+
191+
case MessageDecoder.WM_RBUTTONDBLCLK:
192+
break;
193+
194+
case MessageDecoder.WM_RBUTTONDOWN:
195+
break;
196+
197+
case MessageDecoder.WM_RBUTTONUP:
198+
OnMenuOpening(this, new ValueEventArgs<POINT>(new POINT(x, y)));
199+
CloseNotificationArea();
200+
break;
201+
202+
case Constants.NIN_BALLOONSHOW:
203+
break;
204+
205+
case Constants.NIN_BALLOONHIDE:
206+
break;
207+
208+
case Constants.NIN_BALLOONTIMEOUT:
209+
break;
210+
211+
case Constants.NIN_BALLOONUSERCLICK:
212+
break;
213+
214+
case Constants.NIN_POPUPOPEN:
215+
break;
216+
217+
case Constants.NIN_POPUPCLOSE:
218+
break;
219+
}
220+
}
221+
else if (msg == TaskbarCreatedMessage)
222+
{
223+
_added = false;
224+
UpdateIcon(_visible);
225+
}
226+
else
227+
{
228+
switch (msg)
229+
{
230+
case MessageDecoder.WM_COMMAND:
231+
break;
232+
233+
case MessageDecoder.WM_ACTIVATEAPP:
234+
return 0;
235+
236+
case MessageDecoder.WM_DESTROY:
237+
UpdateIcon(false);
238+
break;
239+
}
240+
}
241+
return DefWindowProc(hWnd, msg, wParam, lParam);
242+
}
243+
244+
private sealed class NotifyIconNativeWindow : IDisposable
245+
{
246+
private static readonly WNDPROC _windowProc = WindowProc;
247+
private static readonly ConcurrentDictionary<IntPtr, NotifyIconNativeWindow> _windows = new();
248+
private static readonly ConcurrentDictionary<IntPtr, NotifyIconNativeWindow> _windowsBeingCreated = new();
249+
private static long _createIndex;
250+
251+
private readonly NotifyIcon _notifyIcon;
252+
private nint _handle;
253+
254+
public NotifyIconNativeWindow(NotifyIcon notifyIcon)
255+
{
256+
_notifyIcon = notifyIcon;
257+
258+
var wc = new WNDCLASSW
259+
{
260+
lpfnWndProc = Marshal.GetFunctionPointerForDelegate(_windowProc),
261+
lpszClassName = PWSTR.From(GetType().FullName)
262+
};
263+
264+
var index = (nint)Interlocked.Increment(ref _createIndex);
265+
_windowsBeingCreated[index] = this;
266+
267+
if (Functions.RegisterClassW(wc) == 0)
268+
{
269+
// we always register the same class name, so "already exists" is expected
270+
var gle = Marshal.GetLastWin32Error();
271+
if ((WIN32_ERROR)gle != WIN32_ERROR.ERROR_CLASS_ALREADY_EXISTS)
272+
throw new Win32Exception(gle);
273+
}
274+
275+
_handle = Functions.CreateWindowExW(
276+
WINDOW_EX_STYLE.WS_EX_NOACTIVATE,
277+
wc.lpszClassName,
278+
PWSTR.From(nameof(NotifyIconNativeWindow)),
279+
0,
280+
Constants.CW_USEDEFAULT, Constants.CW_USEDEFAULT, Constants.CW_USEDEFAULT, Constants.CW_USEDEFAULT,
281+
HWND.Null,
282+
0,
283+
new HINSTANCE { Value = Functions.GetModuleHandleW(PWSTR.Null) },
284+
index);
285+
if (_handle == 0)
286+
throw new Win32Exception(Marshal.GetLastWin32Error());
287+
}
288+
289+
public HWND Handle => _handle;
290+
291+
public void Dispose()
292+
{
293+
var handle = Interlocked.Exchange(ref _handle, 0);
294+
if (handle != 0)
295+
{
296+
Functions.SendMessageW(handle, MessageDecoder.WM_CLOSE, 0, 0);
297+
}
298+
}
299+
300+
private static LRESULT WindowProc(HWND hWnd, uint msg, WPARAM wParam, LPARAM lParam)
301+
{
302+
if (msg == MessageDecoder.WM_CREATE && lParam != 0)
303+
{
304+
var userData = Marshal.ReadIntPtr(lParam);
305+
if (_windowsBeingCreated.TryRemove(userData, out var win))
306+
{
307+
_windows[hWnd] = win;
308+
}
309+
}
310+
311+
_windows.TryGetValue(hWnd, out var nativeWindow);
312+
var notifyIcon = nativeWindow?._notifyIcon;
313+
if (notifyIcon != null)
314+
{
315+
if (msg == MessageDecoder.WM_NCDESTROY)
316+
{
317+
// this is the very last message the window can receive, remove it from the list
318+
_windows.TryRemove(hWnd, out _);
319+
}
320+
return notifyIcon.WindowProc(hWnd, msg, wParam, lParam);
321+
}
322+
323+
return DefWindowProc(hWnd, msg, wParam, lParam);
324+
}
325+
}
326+
327+
protected virtual void Dispose(bool disposing)
328+
{
329+
if (disposing)
330+
{
331+
UpdateIcon(false);
332+
Interlocked.Exchange(ref _window, null)?.Dispose();
333+
_iconHandle = HICON.Null;
334+
}
335+
}
336+
337+
~NotifyIcon() { Dispose(disposing: false); }
338+
public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); }
339+
}

0 commit comments

Comments
 (0)