Skip to content

Commit 606a932

Browse files
Copilotshanselman
andauthored
Add WDA_EXCLUDEFROMCAPTURE support to hide edge light from screen capture (#22)
* Initial plan * Implement WDA_EXCLUDEFROMCAPTURE feature with settings persistence - Add AppSettings class for persistent configuration storage - Add P/Invoke declarations for SetWindowDisplayAffinity Windows API - Implement exclude from capture functionality for main and additional monitor windows - Add tray menu option with checkmark for the feature - Add control toolbar button (🎥) for quick toggle - Persist setting across application restarts using JSON configuration - Update README with comprehensive documentation about screen sharing mode - Update help dialog to mention the new feature Co-authored-by: shanselman <2892+shanselman@users.noreply.github.com> * Add error handling and improve accessibility for exclude from capture feature - Add error handling to SetWindowDisplayAffinity calls with debug logging - Improve JSON deserialization with validation and error recovery - Update button tooltip to clearly indicate current state for better accessibility - Add SetLastError to P/Invoke declaration for proper error reporting Co-authored-by: shanselman <2892+shanselman@users.noreply.github.com> * Fix error handling to capture Win32 error codes immediately - Store error code immediately after API call to ensure accuracy - Add missing System namespace import in AppSettings.cs Co-authored-by: shanselman <2892+shanselman@users.noreply.github.com> * Simplify exclude-from-capture: default ON, remove button from UI --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: shanselman <2892+shanselman@users.noreply.github.com> Co-authored-by: Scott Hanselman <scott@hanselman.com>
1 parent 7fdacd3 commit 606a932

File tree

5 files changed

+364
-2
lines changed

5 files changed

+364
-2
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ A lightweight WPF application that adds a customizable glowing edge light effect
1414
- **Toggle On/Off**: Quickly enable or disable the edge light effect
1515
- **Hideable Controls**: Hide the control toolbar for a cleaner look, restore via tray menu
1616
- **Always On Top**: Stays visible above all other windows
17+
- **Exclude from Screen Capture**: Optional setting to hide the edge light from screen sharing (Teams, Zoom) and screenshots
1718
- **Keyboard Shortcuts**:
1819
- `Ctrl+Shift+L` - Toggle light on/off
1920
- `Ctrl+Shift+Up` - Increase brightness
@@ -85,9 +86,19 @@ The executable will be in `bin\Release\net10.0-windows\win-x64\publish\WindowsEd
8586
- 🔥 **Warmer Color** - Shifts the glow towards a warmer, amber tone
8687
- 💡 **Toggle Light** - Turn the effect on/off
8788
- 🖥️ **Switch Monitor** - Move to next monitor (if multiple monitors)
89+
- 🖥️🖥️ **All Monitors** - Show on all monitors (if multiple monitors)
90+
- 🎥 **Exclude from Capture** - Hide from screen sharing and screenshots
8891
-**Exit** - Close the application
8992
4. Hide the control toolbar for a cleaner look using the tray menu (right-click tray icon → "Hide Controls")
9093

94+
### Screen Sharing Mode
95+
96+
When sharing your screen on video conferencing apps (Teams, Zoom, etc.), you may want the edge light to be visible to you but invisible to viewers. Enable **"Exclude from Screen Capture"** via:
97+
- Click the 🎥 button in the control toolbar, or
98+
- Right-click the tray icon → "Exclude from Screen Capture"
99+
100+
**Note**: When this setting is enabled, the edge light will also be excluded from screenshots taken with Windows Snipping Tool, PrintScreen, or other capture tools.
101+
91102
### Keyboard Shortcuts
92103

93104
- **Ctrl+Shift+L**: Toggle the edge light on/off

WindowsEdgeLight/AppSettings.cs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
using System;
2+
using System.IO;
3+
using System.Text.Json;
4+
5+
namespace WindowsEdgeLight;
6+
7+
/// <summary>
8+
/// Application settings that persist across sessions
9+
/// </summary>
10+
public class AppSettings
11+
{
12+
private static readonly string SettingsFilePath = Path.Combine(
13+
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
14+
"WindowsEdgeLight",
15+
"settings.json");
16+
17+
/// <summary>
18+
/// When enabled, excludes the edge light from screen capture (Teams, screenshots, etc.)
19+
/// Note: When enabled, screenshots won't capture the edge light effect
20+
/// </summary>
21+
public bool ExcludeFromCapture { get; set; } = true;
22+
23+
/// <summary>
24+
/// Load settings from disk
25+
/// </summary>
26+
public static AppSettings Load()
27+
{
28+
try
29+
{
30+
if (File.Exists(SettingsFilePath))
31+
{
32+
var json = File.ReadAllText(SettingsFilePath);
33+
var options = new JsonSerializerOptions
34+
{
35+
AllowTrailingCommas = true,
36+
ReadCommentHandling = JsonCommentHandling.Skip
37+
};
38+
var settings = JsonSerializer.Deserialize<AppSettings>(json, options);
39+
40+
// Validate deserialized settings
41+
if (settings != null)
42+
{
43+
return settings;
44+
}
45+
}
46+
}
47+
catch (JsonException ex)
48+
{
49+
System.Diagnostics.Debug.WriteLine($"Failed to parse settings file: {ex.Message}");
50+
// Delete corrupted settings file
51+
try
52+
{
53+
if (File.Exists(SettingsFilePath))
54+
{
55+
File.Delete(SettingsFilePath);
56+
}
57+
}
58+
catch { /* Ignore deletion errors */ }
59+
}
60+
catch (Exception ex)
61+
{
62+
System.Diagnostics.Debug.WriteLine($"Failed to load settings: {ex.Message}");
63+
}
64+
65+
return new AppSettings();
66+
}
67+
68+
/// <summary>
69+
/// Save settings to disk
70+
/// </summary>
71+
public void Save()
72+
{
73+
try
74+
{
75+
var directory = Path.GetDirectoryName(SettingsFilePath);
76+
if (directory != null && !Directory.Exists(directory))
77+
{
78+
Directory.CreateDirectory(directory);
79+
}
80+
81+
var json = JsonSerializer.Serialize(this, new JsonSerializerOptions
82+
{
83+
WriteIndented = true
84+
});
85+
File.WriteAllText(SettingsFilePath, json);
86+
}
87+
catch (Exception ex)
88+
{
89+
System.Diagnostics.Debug.WriteLine($"Failed to save settings: {ex.Message}");
90+
}
91+
}
92+
}

WindowsEdgeLight/ControlWindow.xaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
44
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
55
Title="Controls"
6-
Width="370"
6+
Width="366"
77
Height="48"
88
AllowsTransparency="True"
99
Background="Transparent"

WindowsEdgeLight/MainWindow.xaml.cs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ public partial class MainWindow : Window
3636
// Tracks whether the control window should be visible (controls initial visibility and toggle state)
3737
private bool isControlWindowVisible = true;
3838
private ToolStripMenuItem? toggleControlsMenuItem;
39+
private ToolStripMenuItem? excludeFromCaptureMenuItem;
40+
41+
// Application settings
42+
private AppSettings settings = new AppSettings();
3943

4044
private class MonitorWindowContext
4145
{
@@ -74,6 +78,12 @@ private class MonitorWindowContext
7478
[DllImport("user32.dll")]
7579
private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
7680

81+
[DllImport("user32.dll", SetLastError = true)]
82+
private static extern bool SetWindowDisplayAffinity(IntPtr hWnd, uint dwAffinity);
83+
84+
private const uint WDA_NONE = 0x00000000;
85+
private const uint WDA_EXCLUDEFROMCAPTURE = 0x00000011;
86+
7787
[DllImport("user32.dll")]
7888
private static extern IntPtr MonitorFromPoint(POINT pt, uint dwFlags);
7989

@@ -141,6 +151,10 @@ public MainWindow()
141151
{
142152
InitializeComponent();
143153
hoverCursorRing = FindName("HoverCursorRing") as Ellipse;
154+
155+
// Load settings
156+
settings = AppSettings.Load();
157+
144158
SetupNotifyIcon();
145159
}
146160

@@ -190,6 +204,12 @@ private void SetupNotifyIcon()
190204
toggleControlsMenuItem = new ToolStripMenuItem("🎛️ Hide Controls", null, (s, e) => ToggleControlsVisibility());
191205
contextMenu.Items.Add(toggleControlsMenuItem);
192206

207+
// Add exclude from capture menu item with checkmark
208+
excludeFromCaptureMenuItem = new ToolStripMenuItem("🎥 Exclude from Screen Capture", null, (s, e) => ToggleExcludeFromCapture());
209+
excludeFromCaptureMenuItem.CheckOnClick = true;
210+
excludeFromCaptureMenuItem.Checked = settings.ExcludeFromCapture;
211+
contextMenu.Items.Add(excludeFromCaptureMenuItem);
212+
193213
contextMenu.Items.Add(new ToolStripSeparator());
194214
contextMenu.Items.Add("✖ Exit", null, (s, e) => System.Windows.Application.Current.Shutdown());
195215

@@ -218,6 +238,7 @@ private void ShowHelp()
218238
• Control toolbar with brightness, color temp, and monitor options
219239
• Color temperature controls (🔥 warmer, ❄️ cooler)
220240
• Switch between monitors or show on all monitors
241+
• Exclude from screen capture (🎥) - invisible in Teams/Zoom sharing
221242
222243
Created by Scott Hanselman
223244
Version {version}";
@@ -297,6 +318,9 @@ private void Window_Loaded(object sender, RoutedEventArgs e)
297318
this.SizeChanged += Window_SizeChanged;
298319
this.LocationChanged += Window_LocationChanged;
299320

321+
// Apply exclude from capture setting
322+
ApplyExcludeFromCapture();
323+
300324
InstallMouseHook();
301325
}
302326

@@ -693,6 +717,65 @@ private void UpdateTrayMenuToggleControlsText()
693717
}
694718
}
695719

720+
public void ToggleExcludeFromCapture()
721+
{
722+
settings.ExcludeFromCapture = !settings.ExcludeFromCapture;
723+
settings.Save();
724+
725+
// Update menu checkmark
726+
if (excludeFromCaptureMenuItem != null)
727+
{
728+
excludeFromCaptureMenuItem.Checked = settings.ExcludeFromCapture;
729+
}
730+
731+
// Apply the setting to all windows
732+
ApplyExcludeFromCapture();
733+
}
734+
735+
private void ApplyExcludeFromCapture()
736+
{
737+
var hwnd = new WindowInteropHelper(this).Handle;
738+
if (hwnd != IntPtr.Zero)
739+
{
740+
var result = SetWindowDisplayAffinity(hwnd, settings.ExcludeFromCapture ? WDA_EXCLUDEFROMCAPTURE : WDA_NONE);
741+
if (!result)
742+
{
743+
var error = Marshal.GetLastWin32Error();
744+
System.Diagnostics.Debug.WriteLine($"Failed to set display affinity for main window. Error: {error}");
745+
}
746+
}
747+
748+
// Apply to control window
749+
if (controlWindow != null)
750+
{
751+
var controlHwnd = new WindowInteropHelper(controlWindow).Handle;
752+
if (controlHwnd != IntPtr.Zero)
753+
{
754+
var result = SetWindowDisplayAffinity(controlHwnd, settings.ExcludeFromCapture ? WDA_EXCLUDEFROMCAPTURE : WDA_NONE);
755+
if (!result)
756+
{
757+
var error = Marshal.GetLastWin32Error();
758+
System.Diagnostics.Debug.WriteLine($"Failed to set display affinity for control window. Error: {error}");
759+
}
760+
}
761+
}
762+
763+
// Apply to all additional monitor windows
764+
foreach (var ctx in additionalMonitorWindows)
765+
{
766+
var monitorHwnd = new WindowInteropHelper(ctx.Window).Handle;
767+
if (monitorHwnd != IntPtr.Zero)
768+
{
769+
var result = SetWindowDisplayAffinity(monitorHwnd, settings.ExcludeFromCapture ? WDA_EXCLUDEFROMCAPTURE : WDA_NONE);
770+
if (!result)
771+
{
772+
var error = Marshal.GetLastWin32Error();
773+
System.Diagnostics.Debug.WriteLine($"Failed to set display affinity for monitor window. Error: {error}");
774+
}
775+
}
776+
}
777+
}
778+
696779
public void IncreaseBrightness()
697780
{
698781
currentOpacity = Math.Min(MaxOpacity, currentOpacity + OpacityStep);
@@ -1030,6 +1113,14 @@ private MonitorWindowContext CreateMonitorWindow(Screen screen)
10301113
int extendedStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
10311114
SetWindowLong(hwnd, GWL_EXSTYLE, extendedStyle | WS_EX_TRANSPARENT | WS_EX_LAYERED);
10321115

1116+
// Apply exclude from capture setting
1117+
var result = SetWindowDisplayAffinity(hwnd, settings.ExcludeFromCapture ? WDA_EXCLUDEFROMCAPTURE : WDA_NONE);
1118+
if (!result)
1119+
{
1120+
var error = Marshal.GetLastWin32Error();
1121+
System.Diagnostics.Debug.WriteLine($"Failed to set display affinity for monitor window during creation. Error: {error}");
1122+
}
1123+
10331124
// Verify and update DPI if WPF reports a different value after window is loaded
10341125
var source = PresentationSource.FromVisual(window);
10351126
if (source != null)
@@ -1109,6 +1200,11 @@ public bool HasMultipleMonitors()
11091200
return availableMonitors.Length > 1;
11101201
}
11111202

1203+
public bool IsExcludeFromCaptureEnabled()
1204+
{
1205+
return settings.ExcludeFromCapture;
1206+
}
1207+
11121208
private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
11131209
{
11141210
// Recreate geometry when window size changes (e.g., different monitor resolution)

0 commit comments

Comments
 (0)