diff --git a/.editorconfig b/.editorconfig new file mode 100755 index 0000000..db44193 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,5 @@ + +root = true + +[*] +end_of_line = lf diff --git a/.gitattributes b/.gitattributes new file mode 100755 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..9491a2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,363 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100755 index 0000000..ceadeed --- /dev/null +++ b/Program.cs @@ -0,0 +1,352 @@ + +/* +*************************************************************************** +Author : arun-goud +Github project : Wintermelon + (WIN)dows (T)en & (E)leven (R)endering of (M)irrored (E)vent-triggered (L)ockscreens (O)n (N)on-main displays +Description : C# program for Win 10 & 11 that toggles projection mode + between duplicate and extend setting. + Accepts optional arguments 'save' and 'restore' that + store and retrieve positions of open windows, respectively, + using a settings file at + C:\Users\Username\AppData\Local\Wintermelon\winpos.txt +Use case : Allows lockscreen mirroring by registering as a task + with task scheduler and using lock, unlock events as + the task triggers. +Created on : 2020/06/19 +Last modified on : 2021/07/05 +References : Code snippets were borrowed from below URLs + https://docs.microsoft.com/en-us/windows/win32/gdi/positioning-objects-on-multiple-display-monitors + https://docs.microsoft.com/en-us/archive/blogs/davidrickard/saving-window-size-and-location-in-wpf-and-winforms + https://bytes.com/topic/net/answers/855274-getting-child-windows-using-process-getprocess + https://stackoverflow.com/questions/15448266/how-do-you-find-what-windowhandle-takes-process-ownership-and-get-the-handle + http://pinvoke.net/default.aspx/user32.EnumDesktopWindows + https://stackoverflow.com/questions/19867402/how-can-i-use-enumwindows-to-find-windows-with-a-specific-caption-title + https://stackoverflow.com/questions/38460253/how-to-use-system-windows-forms-in-net-core-class-library + https://github.com/BiNZGi/WinPos + +*************************************************************************** +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Windows.Forms; + +namespace Wintermelon +{ + class Program + { + // PInvoke imports for controlling Window placement + [DllImport("user32.dll")] + private static extern bool SetWindowPlacement(IntPtr hWnd, [In] ref WINDOWPLACEMENT lpwndpl); + [DllImport("user32.dll")] + private static extern bool GetWindowPlacement(IntPtr hWnd, out WINDOWPLACEMENT lpwndpl); + + // PInvoke imports to get handles to multiple windows having the same process name + public delegate bool EnumDelegate(IntPtr hWnd, int lParam); + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool IsWindowVisible(IntPtr hWnd); + [DllImport("user32.dll")] + public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpWindowText, int nMaxCount); + [DllImport("user32.dll")] + public static extern bool EnumDesktopWindows(IntPtr hDesktop, EnumDelegate lpEnumCallbackFunction, IntPtr lParam); + [DllImport("User32.dll")] + public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + [DllImport("dwmapi.dll")] + public static extern int DwmGetWindowAttribute(IntPtr hwnd, DwmWindowAttribute dwAttribute, out bool pvAttribute, int cbAttribute); + [Flags] + public enum DwmWindowAttribute + { + DWMWA_NCRENDERING_ENABLED = 1, + DWMWA_NCRENDERING_POLICY, + DWMWA_TRANSITIONS_FORCEDISABLED, + DWMWA_ALLOW_NCPAINT, + DWMWA_CAPTION_BUTTON_BOUNDS, + DWMWA_NONCLIENT_RTL_LAYOUT, + DWMWA_FORCE_ICONIC_REPRESENTATION, + DWMWA_FLIP3D_POLICY, + DWMWA_EXTENDED_FRAME_BOUNDS, + DWMWA_HAS_ICONIC_BITMAP, + DWMWA_DISALLOW_PEEK, + DWMWA_EXCLUDED_FROM_PEEK, + DWMWA_CLOAK, + DWMWA_CLOAKED, + DWMWA_FREEZE_REPRESENTATION, + DWMWA_LAST + } + + // Display switch imports + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + private static extern long SetDisplayConfig(uint numPathArrayElements, + IntPtr pathArray, uint numModeArrayElements, IntPtr modeArray, uint flags); + + public enum SDC_FLAGS + { + SDC_TOPOLOGY_INTERNAL = 1, + SDC_TOPOLOGY_CLONE = 2, + SDC_TOPOLOGY_EXTEND = 4, + SDC_TOPOLOGY_EXTERNAL = 8, + SDC_APPLY = 128 + } + + public static void CloneDisplays() + { + SetDisplayConfig(0, IntPtr.Zero, 0, IntPtr.Zero, ((uint) SDC_FLAGS.SDC_APPLY | (uint) SDC_FLAGS.SDC_TOPOLOGY_CLONE)); + } + + public static void ExtendDisplays() + { + SetDisplayConfig(0, IntPtr.Zero, 0, IntPtr.Zero, ((uint) SDC_FLAGS.SDC_APPLY | (uint) SDC_FLAGS.SDC_TOPOLOGY_EXTEND)); + } + + public static void ExternalDisplay() + { + SetDisplayConfig(0, IntPtr.Zero, 0, IntPtr.Zero, ((uint) SDC_FLAGS.SDC_APPLY | (uint) SDC_FLAGS.SDC_TOPOLOGY_EXTERNAL)); + } + + public static void InternalDisplay() + { + SetDisplayConfig(0, IntPtr.Zero, 0, IntPtr.Zero, ((uint) SDC_FLAGS.SDC_APPLY | (uint) SDC_FLAGS.SDC_TOPOLOGY_INTERNAL)); + } + + // RECT structure required by WINDOWPLACEMENT structure + [Serializable] + [StructLayout(LayoutKind.Sequential)] + public struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + public RECT(int left, int top, int right, int bottom) + { + this.Left = left; + this.Top = top; + this.Right = right; + this.Bottom = bottom; + } + public override string ToString() + { + return "(" + this.Left.ToString() + "," + this.Top.ToString() + "," + + this.Right.ToString() + "," + this.Bottom.ToString() + ")"; + } + } + + // POINT structure required by WINDOWPLACEMENT structure + [Serializable] + [StructLayout(LayoutKind.Sequential)] + public struct POINT + { + public int X; + public int Y; + public POINT(int x, int y) + { + this.X = x; + this.Y = y; + } + public override string ToString() + { + return "(" + this.X.ToString() + "," + this.Y.ToString() + ")"; + } + } + + // WINDOWPLACEMENT stores the position, size, and state of a window + [Serializable] + [StructLayout(LayoutKind.Sequential)] + private struct WINDOWPLACEMENT + { + public int length; + public int flags; + public int showCmd; + public POINT minPosition; + public POINT maxPosition; + public RECT normalPosition; + public override string ToString() + { + return "Length=" + length.ToString() + "\tFlags=" + flags.ToString()+"\tCmd="+showCmd.ToString()+ + "\tMin=" + minPosition.ToString() + "\tMax=" + maxPosition.ToString() + "\tRect=" + normalPosition.ToString(); + } + } + + static void Main(string[] args) + { + if (args.Length == 0) + { + //*Console.WriteLine("Usage: Wintermelon [save | restore]"); + return; + } + + string appDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Wintermelon"); + if (!Directory.Exists(appDir)) + Directory.CreateDirectory(appDir); + string settingsFile = Path.Combine(appDir, "winpos.txt"); + + Screen[] screens = Screen.AllScreens; + + // Get collection of .exe processes with top-level desktop windows and save to handleInfo[] + var procSettings = new List(); + var handleInfo = new List(); + EnumDelegate filter = delegate (IntPtr hWnd, int lParam) + { + StringBuilder strbTitle = new StringBuilder(255); + int nLength = GetWindowText(hWnd, strbTitle, strbTitle.Capacity + 1); + string strTitle = strbTitle.ToString(); + + if (IsWindowVisible(hWnd) && string.IsNullOrEmpty(strTitle) == false) + { + //procSettings.Add(strTitle); + WINDOWPLACEMENT wp = new WINDOWPLACEMENT(); + wp.length = Marshal.SizeOf(wp); + GetWindowPlacement(hWnd, out wp); + + uint pid = 0; + GetWindowThreadProcessId(hWnd, out pid); + + //if (wp.flags != 0 && wp.showCmd != 1) + DwmGetWindowAttribute(hWnd, DwmWindowAttribute.DWMWA_CLOAKED, out bool isCloaked, Marshal.SizeOf(typeof(bool))); + if (!isCloaked) // Gets rid of WinRT apps such as Microsoft Store which remain cloaked + { + string exeName = Process.GetProcessById((int)pid).MainModule.FileName; + if (exeName.ToLower().EndsWith(".exe")) // Save only .exe generated window positions + { + var screen = Screen.FromHandle(hWnd); + //Console.WriteLine(screen); + procSettings.Add("Title=" + strTitle + "\tPID=" + pid.ToString() + "\tID=" + hWnd.ToString() + "\tName=" + exeName + + "\t" + wp.ToString() + "\tPrimary=" + screen.Primary.ToString() + "\tDisplay=" + screen.DeviceName); + handleInfo.Add(hWnd); + } + } + } + return true; + }; + // Enumerate visible windows using filter defined above + if (!EnumDesktopWindows(IntPtr.Zero, filter, IntPtr.Zero)) + { + // Get the last Win32 error code + int nErrorCode = Marshal.GetLastWin32Error(); + string strErrMsg = String.Format("EnumDesktopWindows failed with code {0}.", nErrorCode); + throw new Exception(strErrMsg); + } + + // Take appropriate action based on whether "save" or "restore" was passed in + // 'save' --> Save window positions and then clone displays + if (string.Equals(args[0], "save", StringComparison.OrdinalIgnoreCase)) + { + //*Console.WriteLine("Saving window positions to {0}...", settingsFile); + File.WriteAllText(settingsFile, ""); + + foreach (var item in procSettings) + { + File.AppendAllText(settingsFile, item + "\n"); + } + //*Console.WriteLine("Done."); + + // Duplicate screen using DisplaySwitch.exe process (slow approach) + //ProcessStartInfo startInfo = new ProcessStartInfo(); + //startInfo.FileName = @"C:\Windows\System32\DisplaySwitch.exe"; + //startInfo.Arguments = @"/clone"; + //Process.Start(startInfo); + + // Duplicate using SetDisplayConfig() (faster approach) + CloneDisplays(); + } + // 'restore' --> Extend displays and then restore windows + else if (string.Equals(args[0], "restore", StringComparison.OrdinalIgnoreCase)) + { + // Extend using DisplaySwitch.exe process (slow approach) + //Process p = new Process(); + //p.StartInfo.FileName = @"C:\Windows\System32\DisplaySwitch.exe"; + //p.StartInfo.Arguments = @"/extend"; + //p.Start(); + //p.WaitForExit(); + + // Extend using SetDisplayConfig() (faster approach) + ExtendDisplays(); + + + //*Console.WriteLine("Restoring window positions from {0}...", settingsFile); + // Read settings.txt into a dictionary with pid+"_"+ptitle serving as key + var processInfo = new Dictionary(); + System.IO.StreamReader infile = new System.IO.StreamReader(@settingsFile); + string line; + while ((line = infile.ReadLine()) != null) + { + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + //System.Console.WriteLine(line); + var dict = line.Split('\t').Select(x => x.Split('=')).ToDictionary(x => x[0], x => x[1]); + string pid = dict["PID"]; + string id = dict["ID"]; + string ptitle = dict["Title"]; + bool isPrimary = string.Equals(dict["Primary"], "True", StringComparison.OrdinalIgnoreCase); + + // Save only for those that are not on the primary screen + if (!isPrimary) + { + WINDOWPLACEMENT wp = new WINDOWPLACEMENT(); + wp.length = Marshal.SizeOf(wp); + wp.flags = int.Parse(dict["Flags"]); + wp.showCmd = int.Parse(dict["Cmd"]); + var minxy = dict["Min"].Replace("(", "").Replace(")", "").Split(","); + wp.minPosition = new POINT(int.Parse(minxy[0]), int.Parse(minxy[1])); + var maxxy = dict["Max"].Replace("(", "").Replace(")", "").Split(","); + wp.maxPosition = new POINT(int.Parse(maxxy[0]), int.Parse(maxxy[1])); + var pqrs = dict["Rect"].Replace("(", "").Replace(")", "").Split(","); + wp.normalPosition = new RECT(int.Parse(pqrs[0]), int.Parse(pqrs[1]),int.Parse(pqrs[2]), int.Parse(pqrs[3])); + + processInfo.Add(pid + "_" + id + "_" + ptitle, wp); + //Console.WriteLine("Info='{0}'", pid + "_" + id + "_" + ptitle); + } + } + infile.Close(); + + //*Console.WriteLine("Restoring {0} windows", handleInfo.Count()); + + foreach (var handle in handleInfo) + { + StringBuilder strbTitle = new StringBuilder(255); + int nLength = GetWindowText(handle, strbTitle, strbTitle.Capacity + 1); + string ptitle = strbTitle.ToString(); + uint procid = 0; + GetWindowThreadProcessId(handle, out procid); + + string pid = procid.ToString(); + string id = handle.ToString(); + + // Get window positions for the processes from settings.txt, if present, and enforce them + if (processInfo.ContainsKey(pid + "_" + id + "_" + ptitle)) + { + //*Console.WriteLine("Info={0}", pid + "_" + id + "_" + ptitle); + // To restore maximized windows + WINDOWPLACEMENT wp = processInfo[pid + "_" + id + "_" + ptitle]; + wp.length = Marshal.SizeOf(wp); + wp.showCmd = 1; + SetWindowPlacement(handle, ref wp); + + // To restore rest of them + WINDOWPLACEMENT wp2 = processInfo[pid + "_" + id + "_" + ptitle]; + wp2.length = Marshal.SizeOf(wp2); + SetWindowPlacement(handle, ref wp2); + } + + } + + //*Console.WriteLine("Done."); + } + else + { + //*Console.WriteLine("Usage: Wintermelon [save | restore]"); + return; + } + + } + } +} diff --git a/README.md b/README.md index 3aaa387..a88a5d5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,107 @@ # Wintermelon -C# workaround that emulates dual/multi monitor lockscreen feature in Windows 10/11 +***Wintermelon*** is a C# workaround that emulates dual/multi monitor lockscreen feature in Windows 10/11. The ability to duplicate lockscreen existed until Windows 7 but Windows 10 & 11 have chosen to forgo this feature by displaying the lockscreen only on primary (or main) display while the secondary (or non-main) displays go black. + +The name ***Wintermelon*** is an acronym with 2 possible expansions: + +- (**WIN**)dows (**T**)en & (**E**)leven (**R**)endering of (**M**)irrored (**E**)vent-triggered (**L**)ockscreens (**O**)n (**N**)on-main displays + +- (**WIN**)dows (**T**)en & (**E**)leven (**R**)estorer of (**M**)ultiple (**E**)quivalent (**L**)ockscreens (**O**)n (**N**)on-main displays + +Here's a demo showing Wintermelon in action: +![Dual lockscreen on laptop screen and external display implemented on a Win11 machine using Wintermelon](Wintermelon_demo.jpg "Dual lockscreen using Wintermelon") + + +## How it works ## +In Windows 10 & 11, lockscreen will appear on multiple displays only if the display projection mode (accessed by pressing Win+P hotkey) is set to "Duplicate". Most users will typically use primary/secondary (main/non-main) monitors in "Extend" mode to multitask between various opened application windows. Wintermelon sets up a scheduled task that monitors for lock/unlock events. When a lock event is detected the projection mode is switched to "Duplicate" which will duplicate the main display's lockscreen across all non-main displays and when an unlock event is triggered the projection mode reverts to "Extend" mode. +Before switching to "Duplicate" mode the position of opened application windows on the desktop are recorded since the duplication of displays will cause all windows to collapse on to the primary/main display. When reverting to "Extend" mode following the unlock event the recorded positions are retrieved and used to reposition the application windows to their original state. + + +## Requirements ## +Wintermelon is a C# project relying on .NET Core 3.1 and it was originally developed on & for Windows 10 using Visual Studio 2019. + +To build Wintermelon executable you'll need: + +1) .NET Core 3.1 + +2) Visual Studio + +3) Windows machine + +To test and install Wintermelon you'll need: + +1) Windows 10 or Windows 11 machine + +2) PowerShell + +The installer for the C# executable is a PowerShell script which will need to be executed by running it within a PowerShell terminal launched using administrator mode. + + +## Building the executable ## + +1) Open the solution file *Wintermelon.sln* in Visual Studio. The source code resides in *Program.cs*. + +2) Build the solution by choosing **Build --> Build Solution (Ctrl+Shift+B)** in the menu bar. + +3) If there are no build errors then select **Build --> Publish Wintermelon** and pass the following publish information: + + **Target location**=bin\Release\netcoreapp3.1\publish\ + + **Configuration**=Release + + **Delete existing files**=false + + **Target Framework**=netcoreapp3.1 + + **Target Runtime**=win-x64 + + Expand "**Show all**" and set the options under it to following values: + + **Configuration**=Release | Any CPU + + **Target framework**=netcoreapp3.1 + + **Deployment mode**=Framework-dependent + + **Target runtime**=win-x64 + + **Target location**=bin\Release\netcoreapp3.1\publish\ + + + Expand "**File publish options**", set **Produce single file** as checked and leave **Enable ReadyToRun compilation** unchecked. + +4) Press Publish button which will generate the executable **Wintermelon.exe** at the path *bin\Release\netcoreapp3.1\publish\Wintermelon.exe* relative to the C# solution folder. + +This executable handles the switching of projection mode between Duplicate and Extend modes and does recording, retrieval of window positions from a text file saved at *C:\Users\Your_Username\AppData\Local\Wintermelon\winpos.txt*. + +## Using Wintermelon ## +To monitor lock/unlock event and switch the projection mode two separate scheduled tasks will need to be registered with the Windows Task Scheduler. These tasks are: + +- Task 1 : **Wintermelon screen lock save window position clone display** +- Task 2 : **Wintermelon screen unlock extend display restore window** + +The PowerShell scripts *install_wintermelon.ps1* and *uninstall_wintermelon.ps1* will need to be executed to register or unregister the aforementioned tasks, respectively. + +### Registering the Scheduled tasks ### +- Open PowerShell by going to Start Menu, searching for Windows PowerShell and choosing "Run As Administrator" from right lick context menu. + +- To register the tasks run the following command + + `powershell -File install_wintermelon.ps1` + +- Open Task Scheduler from Start Menu and you should see 2 new tasks under Task Scheduler Library with names beginning with *Wintermelon*. The Actions tab for these 2 scheduled tasks will list Wintermelon.exe as the program that should start when these tasks are triggered. Task 1 will invoke Wintermelon.exe by passing the argument 'save' while task 2 will invoke Wintermelon.exe by passing the argument 'restore'. + +- Lock the display by pressing Win+L keys and the lockscreen will appear on all displays. + +- Enter your login credentials to unlock the displays. When the displays are unlocked they are all in duplicate mode momentarily and then the C# app fetches the positions of active application windows and relocates them to the appropriate positions. This requires a 3 second wait and is a limitation of this workaround. + + +### Unregistering the Scheduled tasks ### +- To unregister the tasks run the following command + + `powershell -File uninstall_wintermelon.ps1` + +- Open Task Scheduler from Start Menu and you should no longer see the 2 tasks with names beginning with *Wintermelon* under Task Scheduler Library. + + + + diff --git a/Screen_lock_Save_window_position_Clone_display.xml b/Screen_lock_Save_window_position_Clone_display.xml new file mode 100755 index 0000000..8d1d77e Binary files /dev/null and b/Screen_lock_Save_window_position_Clone_display.xml differ diff --git a/Screen_unlock_Extend_display_Restore_window.xml b/Screen_unlock_Extend_display_Restore_window.xml new file mode 100755 index 0000000..c829ac2 Binary files /dev/null and b/Screen_unlock_Extend_display_Restore_window.xml differ diff --git a/Wintermelon.csproj b/Wintermelon.csproj new file mode 100755 index 0000000..61221d3 --- /dev/null +++ b/Wintermelon.csproj @@ -0,0 +1,12 @@ + + + + WinExe + netcoreapp3.1 + true + true + true + win-x64 + + + diff --git a/Wintermelon.sln b/Wintermelon.sln new file mode 100755 index 0000000..3ed85df --- /dev/null +++ b/Wintermelon.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30204.135 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wintermelon", "Wintermelon.csproj", "{39465C18-3060-49FF-B682-A14875CFDFFF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {39465C18-3060-49FF-B682-A14875CFDFFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {39465C18-3060-49FF-B682-A14875CFDFFF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39465C18-3060-49FF-B682-A14875CFDFFF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {39465C18-3060-49FF-B682-A14875CFDFFF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {02097D8C-38D0-43F1-8BCF-43BFACDE7D4C} + EndGlobalSection +EndGlobal diff --git a/Wintermelon_demo.jpg b/Wintermelon_demo.jpg new file mode 100755 index 0000000..ac56f4e Binary files /dev/null and b/Wintermelon_demo.jpg differ diff --git a/install_wintermelon.ps1 b/install_wintermelon.ps1 new file mode 100755 index 0000000..22b0748 --- /dev/null +++ b/install_wintermelon.ps1 @@ -0,0 +1,45 @@ +#Requires -RunAsAdministrator + +# PowerShell script to register Wintermelon with Windows Task Scheduler +# by running the command below in PowerShell terminal launched in admin mode +# powershell -File "Path to this script" + +# Step 1: Enable audit of lock/unlock eventIDs 4800, 4801 using secpol.msc +# Windows search -> Local Security Policy (secpol.msc) +# -> Advanced Audit Policy Configuration +# -> System Audit Policies - Local Group Policy Object +# -> Logon/Logoff +# -> Audit Other Logon/Logoff Events = Success +# Step 2: Set up scheduled tasks using template XML files for lock/unlock event + + +# Step 1 +auditpol /set /subcategory:"Other Logon/Logoff Events" /success:enable /failure:enable + +# Step 2 +$scriptdir = Split-Path -Parent $PSCommandPath +$xml1 = $scriptdir + "\Screen_lock_Save_window_position_Clone_display.xml" +$xml2 = $scriptdir + "\Screen_unlock_Extend_display_Restore_window.xml" +$user = "$env:USERDOMAIN\$env:USERNAME" +$timestamp = Get-Date -format s + +# Customize XML1 and then register with Task Scheduler +[xml]$schtask1 = Get-Content $xml1 +$schtask1.Task.RegistrationInfo.Date = $timestamp.ToString() +$schtask1.Task.RegistrationInfo.Author = $user +$schtask1.Task.Principals.Principal.UserId = $user +$schtask1.Task.Actions.Exec.Command = $scriptdir + "\bin\Release\netcoreapp3.1\publish\Wintermelon.exe" +$schtask1.Save($xml1) +Write-Host "Setting up task 1 - Wintermelon screen lock save window position clone display..." +Register-ScheduledTask -Xml (Get-Content $xml1 | Out-String) -TaskName "Wintermelon screen lock save window position clone display" -User $user + +# Customize XML2 and then register with Task Scheduler +[xml]$schtask2 = Get-Content $xml2 +$schtask2.Task.RegistrationInfo.Date = $timestamp.ToString() +$schtask2.Task.RegistrationInfo.Author = $user +$schtask2.Task.Principals.Principal.UserId = $user +$schtask2.Task.Actions.Exec.Command = $scriptdir + "\bin\Release\netcoreapp3.1\publish\Wintermelon.exe" +$schtask2.Save($xml2) +Write-Host "Setting up task 2 - Wintermelon screen unlock extend display restore window..." +Register-ScheduledTask -Xml (Get-Content $xml2 | Out-String) -TaskName "Wintermelon screen unlock extend display restore window" -User $user + diff --git a/uninstall_wintermelon.ps1 b/uninstall_wintermelon.ps1 new file mode 100755 index 0000000..a115982 --- /dev/null +++ b/uninstall_wintermelon.ps1 @@ -0,0 +1,25 @@ +#Requires -RunAsAdministrator + +# PowerShell script to unregister Wintermelon with Windows Task Scheduler +# in admin mode +$task1 = "Wintermelon screen lock save window position clone display" +$task2 = "Wintermelon screen unlock extend display restore window" + +# Unregister task1 with Task Scheduler +Write-Host "Removing task 1 - Wintermelon screen lock save window position clone display..." +if ($(Get-ScheduledTask -TaskName $task1 -ErrorAction SilentlyContinue).TaskName -eq $task1) { + Unregister-ScheduledTask -TaskName $task1 -Confirm:$false +} else { + Write-Host "No such task exists." +} + + + +# Unregister task2 with Task Scheduler +Write-Host "Removing task 2 - Wintermelon screen unlock extend display restore window..." +if ($(Get-ScheduledTask -TaskName $task2 -ErrorAction SilentlyContinue).TaskName -eq $task2) { + Unregister-ScheduledTask -TaskName $task2 -Confirm:$false +} else { + Write-Host "No such task exists." +} +