diff --git a/README.md b/README.md index 5516178..9f01b42 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,74 @@ # Windows-RoundedScreen + +**THIS PROJECT IS NOT MAINTAINED BUT I FREQUENTLY CHECK THE PULL REQUESTS** + A simple workaround to get rounded screen corners on Windows. -LATEST CHANGES (26/01/2023) : +> 🖥️ [Download RoundedScreen.exe](https://github.com/BeezBeez/Windows-RoundedScreen/releases/latest/download/RoundedScreen.exe) + +## Customize rounded corners + +The program runs in the Windows taskbar. + +Right-clicking the program provides options for customizing the rounded corners style and radius: + +![](png.png) + +Your selections are automatically saved, and will be the same when you reopen the program. + +## Troubleshooting + +**Program doesn't appear in taskbar on Windows 11** + +If you are using Windows 11, you will need to add the program to your taskbar: + +1. Run the program +2. Right-click your taskbar and select "Taskbar settings" +3. In the Settings select "Other system tray icons" +4. Enable the system tray icon for "RoundedScreen" + +# Changelog + +## Latest changes + - hidden from alt+tab list - made corners a bit smaller - added an AppIcon - upped the version number - added a command to quit the program +- added program to taskbar with corner size options +- added superellipse rounding style for smoother corners +- customization of corner style and size is now saved between sessions -**THIS PROJECT IS NOT MAINTAINED BUT I FREQUENTLY CHECK THE PULL REQUESTS** +# How to build this project + +## Prerequisites + +- **Visual Studio 2022 Build Tools** with the Managed Desktop workload +- **.NET Framework 4.7.2 Targeting Pack** (installed as a component of Build Tools) + +## Install + +Install prerequisites: + +```bat +install.bat +``` + +## Build + +Build the program: + +```bat +build.bat +``` + +Program is built to `RoundedScreen/bin/Release/RoundedScreen.exe`. + +## Run + +Run the program after building: + +```bat +run.bat +``` \ No newline at end of file diff --git a/RoundedScreen/App.xaml.cs b/RoundedScreen/App.xaml.cs index b88ddbc..7dcb398 100644 --- a/RoundedScreen/App.xaml.cs +++ b/RoundedScreen/App.xaml.cs @@ -1,17 +1,115 @@ -using System; +using System; using System.Collections.Generic; using System.Configuration; using System.Data; using System.Linq; using System.Threading.Tasks; using System.Windows; +using System.Windows.Forms; namespace RoundedScreen { /// /// Logique d'interaction pour App.xaml /// - public partial class App : Application + public partial class App : System.Windows.Application { + private NotifyIcon _trayIcon; + + protected override void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + + _trayIcon = new NotifyIcon(); + _trayIcon.Icon = System.Drawing.Icon.ExtractAssociatedIcon(System.Windows.Forms.Application.ExecutablePath); + _trayIcon.Visible = true; + _trayIcon.Text = "RoundedScreen"; + + var menu = new ContextMenuStrip(); + + void AddSizeItem(string text, int size) + { + var item = new ToolStripMenuItem(text); + item.Click += (s, a) => ApplySize(size); + menu.Items.Add(item); + } + + menu.Items.Add(new ToolStripLabel("Corner size")); + menu.Items.Add(new ToolStripSeparator()); + int[] presets = new int[] { 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 28, 32, 40, 48, 56, 64 }; + foreach (var size in presets) + { + AddSizeItem($"{size} px", size); + } + menu.Items.Add(new ToolStripSeparator()); + + // Rounding mode section + menu.Items.Add(new ToolStripLabel("Rounding mode")); + var simpleItem = new ToolStripMenuItem("Simple (Circle)") { CheckOnClick = true }; + var smoothItem = new ToolStripMenuItem("Smooth (Squircle)") { CheckOnClick = true }; + + // Initialize checked state from registry + var wndInit = Current.Windows.OfType().FirstOrDefault(); + var currentMode = wndInit?.ReadRoundingMode() ?? RoundedScreen.MainWindow.RoundingMode.Smooth; + simpleItem.Checked = currentMode == RoundedScreen.MainWindow.RoundingMode.Simple; + smoothItem.Checked = currentMode == RoundedScreen.MainWindow.RoundingMode.Smooth; + + void ApplyMode(RoundedScreen.MainWindow.RoundingMode mode) + { + var wnd = Current.Windows.OfType().FirstOrDefault(); + if (wnd != null) + { + wnd.SaveRoundingMode(mode); + int size = wnd.ReadCornerSize(); + wnd.ApplyCornerSize(size); + } + } + + simpleItem.Click += (s, a) => + { + simpleItem.Checked = true; + smoothItem.Checked = false; + ApplyMode(RoundedScreen.MainWindow.RoundingMode.Simple); + }; + smoothItem.Click += (s, a) => + { + simpleItem.Checked = false; + smoothItem.Checked = true; + ApplyMode(RoundedScreen.MainWindow.RoundingMode.Smooth); + }; + + menu.Items.Add(simpleItem); + menu.Items.Add(smoothItem); + menu.Items.Add(new ToolStripSeparator()); + + var exitItem = new ToolStripMenuItem("Exit"); + exitItem.Click += (s, a) => ExitApplication(); + menu.Items.Add(exitItem); + + _trayIcon.ContextMenuStrip = menu; + } + + private void ApplySize(int size) + { + var wnd = Current.Windows.OfType().FirstOrDefault(); + if (wnd != null) + { + wnd.ApplyCornerSize(size); + wnd.SaveCornerSize(size); + } + } + + private void ExitApplication() + { + var wnd = Current.Windows.OfType().FirstOrDefault(); + if (wnd != null) + { + wnd.AllowClose(); + wnd.Close(); + } + _trayIcon.Visible = false; + _trayIcon.Dispose(); + Shutdown(); + } } } diff --git a/RoundedScreen/MainWindow.xaml b/RoundedScreen/MainWindow.xaml index d329f3c..1867b76 100644 --- a/RoundedScreen/MainWindow.xaml +++ b/RoundedScreen/MainWindow.xaml @@ -1,4 +1,4 @@ - - - - + + - + - - - - + + + + - + - - - - + + + + - + + + + + + + + + + + - - + + diff --git a/RoundedScreen/MainWindow.xaml.cs b/RoundedScreen/MainWindow.xaml.cs index 72ad35e..ef6207c 100644 --- a/RoundedScreen/MainWindow.xaml.cs +++ b/RoundedScreen/MainWindow.xaml.cs @@ -1,10 +1,12 @@ -using Microsoft.Win32; +using Microsoft.Win32; using System; using System.Diagnostics; +using System.ComponentModel; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Input; using System.Windows.Interop; +using System.Windows.Media; namespace RoundedScreen { @@ -13,6 +15,18 @@ public partial class MainWindow : Window public const int WS_EX_TRANSPARENT = 0x00000020; public const int GWL_EXSTYLE = (-20); public const int WS_EX_TOOLWINDOW = 0x00000080; + private const string RegistryKeyPath = "SOFTWARE\\RoundedScreen"; + private const string CornerSizeValueName = "CornerSize"; + private const string RoundingModeValueName = "RoundingMode"; // 0=Simple(circle), 1=Smooth(squircle) + private const int DefaultCornerSize = 28; + private const int DefaultRoundingMode = 1; // Smooth squircle by default + private bool _allowClose = false; + + public enum RoundingMode + { + Simple = 0, + Smooth = 1, + } [DllImport("user32.dll")] public static extern int GetWindowLong(IntPtr hwnd, int index); @@ -24,6 +38,9 @@ public MainWindow() { InitializeComponent(); this.SetStartup(); + // React to display/resolution/orientation changes + SystemEvents.DisplaySettingsChanged += OnDisplaySettingsChanged; + SystemParameters.StaticPropertyChanged += SystemParameters_StaticPropertyChanged; } private void SetStartup() @@ -41,7 +58,21 @@ protected override void OnSourceInitialized(EventArgs e) SetWindowLong(hwnd, GWL_EXSTYLE, extendedStyle | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW); } - private void WndRoundedScreen_Closing(object sender, System.ComponentModel.CancelEventArgs e) { e.Cancel = true; } + protected override void OnClosed(EventArgs e) + { + // Unsubscribe from static events to avoid memory leaks + SystemEvents.DisplaySettingsChanged -= OnDisplaySettingsChanged; + SystemParameters.StaticPropertyChanged -= SystemParameters_StaticPropertyChanged; + base.OnClosed(e); + } + + private void WndRoundedScreen_Closing(object sender, System.ComponentModel.CancelEventArgs e) + { + if (!_allowClose) + { + e.Cancel = true; + } + } private void WndRoundedScreen_LostFocus(object sender, RoutedEventArgs e) { @@ -58,6 +89,184 @@ private void WndRoundedScreen_Loaded(object sender, RoutedEventArgs e) this.Width = System.Windows.SystemParameters.PrimaryScreenWidth; this.Height = System.Windows.SystemParameters.PrimaryScreenHeight; + + int size = ReadCornerSize(); + ApplyCornerSize(size); + } + + private void OnDisplaySettingsChanged(object sender, EventArgs e) + { + // Ensure UI-thread update + Dispatcher.Invoke(ReapplyRoundingAndLayout); + } + + private void SystemParameters_StaticPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(SystemParameters.PrimaryScreenWidth) || + e.PropertyName == nameof(SystemParameters.PrimaryScreenHeight)) + { + Dispatcher.Invoke(ReapplyRoundingAndLayout); + } + } + + private void ReapplyRoundingAndLayout() + { + this.WindowStartupLocation = WindowStartupLocation.Manual; + this.Left = 0; + this.Top = 0; + this.Width = System.Windows.SystemParameters.PrimaryScreenWidth; + this.Height = System.Windows.SystemParameters.PrimaryScreenHeight; + + int size = ReadCornerSize(); + ApplyCornerSize(size); + + this.InvalidateVisual(); + this.UpdateLayout(); + } + + private static Geometry CreateSquircleCornerGeometry(double size, int samples = 36) + { + // Quarter superellipse (a=b=size) with exponent n≈4 for Apple-like squircle + // x = a * cos^(2/n)(t), y = b * sin^(2/n)(t), t ∈ [0, π/2] + // Build a composite geometry: Outer rectangle (0,0,size,size) MINUS the inner squircle curve + // This ensures the filled area is only the corner wedge, not the inner curve area. + + if (samples < 8) samples = 8; + + var geo = new StreamGeometry { FillRule = FillRule.EvenOdd }; + using (var ctx = geo.Open()) + { + // 1) Outer rectangle figure + ctx.BeginFigure(new Point(0, 0), isFilled: true, isClosed: true); + ctx.LineTo(new Point(size, 0), isStroked: false, isSmoothJoin: false); + ctx.LineTo(new Point(size, size), isStroked: false, isSmoothJoin: false); + ctx.LineTo(new Point(0, size), isStroked: false, isSmoothJoin: false); + + // 2) Inner squircle figure (hole) + ctx.BeginFigure(new Point(size, 0), isFilled: true, isClosed: true); + + double n = 4.0; // squircle exponent + for (int i = 1; i <= samples; i++) + { + double t = (Math.PI / 2.0) * (i / (double)samples); + double ct = Math.Cos(t); + double st = Math.Sin(t); + double x = size * Math.Pow(Math.Abs(ct), 2.0 / n); + double y = size * Math.Pow(Math.Abs(st), 2.0 / n); + ctx.LineTo(new Point(x, y), isStroked: false, isSmoothJoin: true); + } + + ctx.LineTo(new Point(0, size), isStroked: false, isSmoothJoin: false); + ctx.LineTo(new Point(0, 0), isStroked: false, isSmoothJoin: false); + ctx.LineTo(new Point(size, 0), isStroked: false, isSmoothJoin: false); + } + + geo.Freeze(); + return geo; + } + + private static Geometry CreateCircularCornerGeometry(double size, int samples = 0) + { + // EvenOdd: outer square (0,0,size,size) minus a quarter circle of radius 'size' from (size,0) to (0,size) + var geo = new StreamGeometry { FillRule = FillRule.EvenOdd }; + using (var ctx = geo.Open()) + { + // Outer square + ctx.BeginFigure(new Point(0, 0), isFilled: true, isClosed: true); + ctx.LineTo(new Point(size, 0), false, false); + ctx.LineTo(new Point(size, size), false, false); + ctx.LineTo(new Point(0, size), false, false); + + // Inner quarter circle (hole): start at (size,0), arc to (0,size) + ctx.BeginFigure(new Point(size, 0), isFilled: true, isClosed: true); + var arc = new ArcSegment(new Point(0, size), new Size(size, size), 0, false, SweepDirection.Clockwise, isStroked: false); + ctx.ArcTo(arc.Point, arc.Size, arc.RotationAngle, arc.IsLargeArc, arc.SweepDirection, isStroked: false, isSmoothJoin: true); + ctx.LineTo(new Point(0, 0), false, false); + ctx.LineTo(new Point(size, 0), false, false); + } + geo.Freeze(); + return geo; + } + + public void ApplyCornerSize(int size) + { + if (size < 4) size = 4; + if (size > 64) size = 64; + + // Update path sizes + pathCornerTL.Width = size; + pathCornerTL.Height = size; + pathCornerTR.Width = size; + pathCornerTR.Height = size; + pathCornerBL.Width = size; + pathCornerBL.Height = size; + pathCornerBR.Width = size; + pathCornerBR.Height = size; + + // Choose geometry based on rounding mode + var mode = ReadRoundingMode(); + Geometry geo = mode == RoundingMode.Smooth + ? CreateSquircleCornerGeometry(size) + : CreateCircularCornerGeometry(size); + pathCornerTL.Data = geo; + pathCornerTR.Data = geo; + pathCornerBL.Data = geo; + pathCornerBR.Data = geo; + } + + public void SaveCornerSize(int size) + { + using (var key = Registry.CurrentUser.CreateSubKey(RegistryKeyPath)) + { + key.SetValue(CornerSizeValueName, size, RegistryValueKind.DWord); + } + } + + public int ReadCornerSize() + { + using (var key = Registry.CurrentUser.CreateSubKey(RegistryKeyPath)) + { + object value = key.GetValue(CornerSizeValueName, DefaultCornerSize); + try + { + return Convert.ToInt32(value); + } + catch + { + return DefaultCornerSize; + } + } + } + + public void SaveRoundingMode(RoundingMode mode) + { + using (var key = Registry.CurrentUser.CreateSubKey(RegistryKeyPath)) + { + key.SetValue(RoundingModeValueName, (int)mode, RegistryValueKind.DWord); + } + } + + public RoundingMode ReadRoundingMode() + { + using (var key = Registry.CurrentUser.CreateSubKey(RegistryKeyPath)) + { + object value = key.GetValue(RoundingModeValueName, DefaultRoundingMode); + try + { + int v = Convert.ToInt32(value); + if (v != 0 && v != 1) return (RoundingMode)DefaultRoundingMode; + return (RoundingMode)v; + } + catch + { + return (RoundingMode)DefaultRoundingMode; + } + } + } + + public void AllowClose() + { + _allowClose = true; } } } diff --git a/RoundedScreen/RoundedScreen.csproj b/RoundedScreen/RoundedScreen.csproj index 99b8216..b674b7d 100644 --- a/RoundedScreen/RoundedScreen.csproj +++ b/RoundedScreen/RoundedScreen.csproj @@ -1,4 +1,4 @@ - + @@ -68,6 +68,7 @@ + diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..1da3217 --- /dev/null +++ b/build.bat @@ -0,0 +1,26 @@ +@echo off +setlocal + +set "MSBUILD_PATH=%ProgramFiles(x86)%\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\MSBuild.exe" + +if not exist "%MSBUILD_PATH%" ( + set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" + if exist "%VSWHERE%" ( + for /f "usebackq delims=" %%i in (`"%VSWHERE%" -latest -products * -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe`) do set "MSBUILD_PATH=%%i" + ) +) + +if not exist "%MSBUILD_PATH%" ( + echo MSBuild not found. Ensure Visual Studio 2022 Build Tools are installed. + exit /b 1 +) + +"%MSBUILD_PATH%" "RoundedScreen.sln" /p:Configuration=Release /m +set "ERR=%ERRORLEVEL%" +if not "%ERR%"=="0" ( + echo Build failed with exit code %ERR%. + exit /b %ERR% +) + +echo Build succeeded. Output: RoundedScreen\bin\Release\RoundedScreen.exe +endlocal diff --git a/install.bat b/install.bat new file mode 100644 index 0000000..47ba696 --- /dev/null +++ b/install.bat @@ -0,0 +1,12 @@ +@echo off +setlocal + +:: Install Visual Studio 2022 Build Tools with Managed Desktop workload and .NET 4.7.2 targeting pack +winget install -e --id Microsoft.VisualStudio.2022.BuildTools --override "--quiet --wait --norestart --add Microsoft.VisualStudio.Workload.ManagedDesktopBuildTools --add Microsoft.Net.Component.4.7.2.TargetingPack --includeRecommended" + +if %ERRORLEVEL% NEQ 0 ( + echo. + echo Installation may have failed or no upgrade was available. Review winget output above. +) + +endlocal diff --git a/png.png b/png.png new file mode 100644 index 0000000..f81b2ac Binary files /dev/null and b/png.png differ diff --git a/run.bat b/run.bat new file mode 100644 index 0000000..a9c9730 --- /dev/null +++ b/run.bat @@ -0,0 +1,12 @@ +@echo off +setlocal + +set EXE=RoundedScreen\bin\Release\RoundedScreen.exe +if not exist "%EXE%" ( + echo Build output not found at %EXE%. + echo Run build.bat first. + exit /b 1 +) + +"%EXE%" +endlocal