-
Notifications
You must be signed in to change notification settings - Fork 2
/
TrayIconBuster.cs
207 lines (185 loc) · 9.42 KB
/
TrayIconBuster.cs
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
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.ComponentModel;
namespace TrayIconBuster
{
/// <remarks>
/// I started with https://www.codeproject.com/Articles/19620/LP-TrayIconBuster as a base, but changed it to use the icon removal method from this script:
/// https://techforpassion.blogspot.com/2014/04/refresh-tray-icons-how-to-remove-dead-tray-icons-after-killing-program.html (he isn't sure where it came
/// from, and I was unable to find an original source either). Using Shell_NotifyIcon(NIM_DELETE,...) seems to work better for me - the old
/// SendMessage(hWnd, TB_DELETEBUTTON,...) call would leave a blank spot where the icon used to be, but this does not.
/// </remarks>
public static class TrayIconBuster
{
private const uint TB_BUTTONCOUNT = 0x0418; // WM_USER+24
private const uint TB_GETBUTTON = 0x0417; // WM_USER+23
/// <summary>
/// List of nested window class hierarchies leading to the toolbar windows. Slashes separate each class name
/// from least to most specific (they should end on ToolbarWindow32). Each of these toolbars will have its
/// phantom icons removed.
/// </summary>
private static readonly List<string> TrayWindowSearchList = new List<string>
{
//The regular toolbar window
"Shell_TrayWnd/TrayNotifyWnd/SysPager/ToolbarWindow32",
//The overflow window
"NotifyIconOverflowWindow/ToolbarWindow32",
};
/// <summary>
/// Removes any zombie icons (those whose processes no longer exist) from the notification tray.
/// </summary>
/// <returns>The number of tray icons removed.</returns>
public static uint RemoveZombieIcons()
{
var toolbarButton = new ToolbarButton();
var extraData = new ExtraButtonData();
uint totalRemovedCount = 0;
uint totalItemCount = 0;
foreach (var windowList in TrayWindowSearchList)
{
//Get a handle to the toolbar window
IntPtr toolbarHandle = FindNestedWindow(windowList);
if (toolbarHandle == IntPtr.Zero)
{
Utilities.Log($"No window found for list: {windowList}");
continue;
}
//Use that handle to open the toolbar's process
using (LP_Process process = new LP_Process(toolbarHandle))
{
//Allocate shared memory in the process to store toolbar button data for us to read
IntPtr remoteButtonPtr = process.Allocate(toolbarButton);
process.Allocate(extraData);
//Ask the window how many buttons it contains so we can iterate over them
uint itemCount = (uint)SendMessage(toolbarHandle, TB_BUTTONCOUNT, IntPtr.Zero, IntPtr.Zero);
totalItemCount += itemCount;
Utilities.Log($"Found {itemCount} tray icons (some may be hidden)");
uint removedCount = 0;
for (uint item = 0; item < itemCount; item++)
{
//We remove items starting from the leftmost (#0), so for each item we have removed, the remaining items' indices
// will have 1 subtracted from them (since they all just got shifted left by 1).
uint index = item - removedCount;
//Get info for this toolbar button
if ((uint)SendMessage(toolbarHandle, TB_GETBUTTON, new IntPtr(index), remoteButtonPtr) == 0) throw new ApplicationException("TB_GETBUTTON failed");
process.Read(toolbarButton, remoteButtonPtr);
process.Read(extraData, toolbarButton.dwData);
//Open the handle for this button to see if its parent process exists or not. If it has no parent, it's a zombie - kill it!
IntPtr buttonHandle = extraData.hWnd;
if (buttonHandle == IntPtr.Zero) throw new ApplicationException("Invalid handle in tray button data");
using (LP_Process proc = new LP_Process(buttonHandle))
{
if (proc.ownerProcessID == 0)
{
RemoveIcon(extraData);
removedCount++;
totalRemovedCount++;
}
}
}
}
}
Utilities.Log($"Done. {totalItemCount} icons found, {totalRemovedCount} icons removed.");
return totalRemovedCount;
}
private static IntPtr FindNestedWindow(string windows)
{
var windowList = windows.Split('/');
IntPtr hWnd = IntPtr.Zero;
foreach (var name in windowList)
{
if (hWnd == IntPtr.Zero)
{
hWnd = FindWindow(name, null);
}
else
{
hWnd = FindWindowEx(hWnd, IntPtr.Zero, name, null);
}
//If we get down here and haven't found a window, there's no point trying to continue - return null.
if (hWnd == IntPtr.Zero) break;
}
return hWnd;
}
//See Shell_NotifyIcon function: https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159(v=vs.85).aspx
private const uint NIM_DELETE = 0x00000002;
/// <summary>
/// Removes the given tray icon using the TrayData we've fetched for it.
/// </summary>
private static void RemoveIcon(ExtraButtonData td)
{
var data = new NotifyIconData()
{
hWnd = td.hWnd,
uID = td.uID
};
//Try to remove the icon. Throw the last Win32 error if this fails.
if (!Shell_NotifyIcon(NIM_DELETE, data)) throw new Win32Exception();
}
[DllImport("user32.dll", EntryPoint = "SendMessage", CallingConvention = CallingConvention.StdCall)]
private static extern IntPtr SendMessage(IntPtr Hdc, uint Msg_Const, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", EntryPoint = "FindWindow", CallingConvention = CallingConvention.StdCall)]
private static extern IntPtr FindWindow(string lpszClass, string lpszWindow);
[DllImport("user32.dll", EntryPoint = "FindWindowEx", CallingConvention = CallingConvention.StdCall)]
private static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
[DllImport("shell32.dll", EntryPoint = "Shell_NotifyIcon", CallingConvention = CallingConvention.StdCall, SetLastError = true)]
public static extern bool Shell_NotifyIcon(uint dwMessage, NotifyIconData pnid);
/// <summary>
/// ToolBarButton struct used for TB_GETBUTTON message.
/// </summary>
/// <remarks>
/// We use a class so LP_Process.Read can fill this.
/// See TBBUTTON struct: http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476(v=vs.85).aspx
/// </remarks>
[StructLayout(LayoutKind.Sequential)]
private class ToolbarButton
{
public uint iBitmap; // 0
public uint idCommand; // 4
public byte fsState; // 8
public byte fsStyle; // 9
public IntPtr dwData; // 12 or 16: points to tray data
public uint iString; // 16 or 24
}
/// <summary>
/// Struct used for extra info for ToolBarButton.
/// </summary>
/// <remarks>
/// We use a class so LP_Process.Read can fill this. This struct apparently is undocumented since it's application-specific.
/// There is likely more to it, but these are the only fields we care about.
/// </remarks>
[StructLayout(LayoutKind.Sequential)]
private class ExtraButtonData
{
public IntPtr hWnd; // 0
public uint uID; // 4 or 8
}
/// <summary>
/// Struct used to make Shell_NotifyIcon calls.
/// See NOTIFYICONDATA struct: https://msdn.microsoft.com/en-us/library/windows/desktop/bb773352(v=vs.85).aspx
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public class NotifyIconData
{
public uint cbSize = (uint)Marshal.SizeOf(typeof(NotifyIconData));
public IntPtr hWnd;
public uint uID;
public uint uFlags;
public uint uCallbackMessage;
public IntPtr hIcon;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] //This size should be 64 if running on something earlier than Win2k
public string szTip;
public int dwState;
public int dwStateMask;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public string szInfo;
public uint uTimeoutOrVersion;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
public string szInfoTitle;
public int dwInfoFlags;
public Guid guidItem;
public IntPtr hBalloonIcon;
}
}
}