Skip to content

Instantly share code, notes, and snippets.

@jdhenckel
Last active February 20, 2024 14:11
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jdhenckel/129f24228be14df8d0de54ad61c03150 to your computer and use it in GitHub Desktop.
Save jdhenckel/129f24228be14df8d0de54ad61c03150 to your computer and use it in GitHub Desktop.
This is a simple DotNet console application to save/restore desktop window positions and sizes
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Newtonsoft.Json;
using System.IO;
using System.Text.RegularExpressions;
using System.Text;
using System.Threading;
namespace Fix_my_windows
{
public struct Rect
{
public int Left { get; set; }
public int Top { get; set; }
public int Right { get; set; }
public int Bottom { get; set; }
public override string ToString()
{
return string.Format("pos({0}, {1}) size({2}, {3})",Left,Top,Right-Left,Bottom-Top);
}
}
public class Item
{
public string Title { get; set; }
public string Setup { get; set; }
[JsonIgnore]
public IntPtr WindowHandle { get; set; }
public int[] GetValues()
{
var pattern = @"pos\s*\(\s*(-?\d+)\s*,\s*(-?\d+)\s*\)(\s*size\s*\(\s*(-?\d+)\s*,\s*(-?\d+)\s*\))?";
var m = new Regex(pattern).Matches(Setup);
if (m.Count == 0)
throw new Exception("Error parsing setup string: " + Setup);
var g = m[0].Groups;
return new int[]
{
int.Parse(g[1].Value),
int.Parse(g[2].Value),
int.Parse(g.Count > 4 ? g[4].Value: "0"),
int.Parse(g.Count > 5 ? g[5].Value: "0"),
};
}
}
class Program
{
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr FindWindow(string strClassName, string strWindowName);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")]
static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
delegate bool EnumThreadDelegate(IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll")]
static extern bool EnumThreadWindows(int dwThreadId, EnumThreadDelegate lpfn, IntPtr lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern int GetWindowText(IntPtr hWnd, StringBuilder title, int size);
[DllImport("user32.dll")]
public static extern bool GetWindowRect(IntPtr hwnd, ref Rect rectangle);
const int MAXTITLE = 1000;
[DllImport("user32.dll")]
public static extern bool IsIconic(IntPtr hwnd);
[DllImport("user32.dll")]
public static extern bool IsZoomed(IntPtr hwnd);
[DllImport("user32.dll")]
public static extern bool IsWindowVisible(IntPtr hwnd);
const uint SWP_NOSIZE = 0x0001;
const uint SWP_NOZORDER = 0x0004;
static void Main(string[] args)
{
var default_filename = "Fix_my_windows.json";
var self = new Program();
if (args.Length > 0 && (args[0] == "-h" || args[0] == "?" ||
args[0] == "--help" || args[0] == "/?" || args[0] == "/h"))
{
Console.WriteLine(@"Save/Restore window positions. John Henckel, April 2022
The default action is to restore positions from Fix_my_windows.json.
You can specify a different filename on the command line.
To save all the current window positions, use -s filename.
When you save, it will record the FULL title of each window. However, during
the restore it will allow partial matches. After you save, you should open
the filename in a text editor and shorten the Title value to something that
is less specific. The setup size is optional. Minimum size is 4,4.
");
}
else if (args.Length > 0 && args[0] == "-s")
{
self.SavePositions(args.Length > 1 ? args[1] : default_filename);
}
else
{
self.RestorePositions(args.Length > 0 ? args[0] : default_filename);
}
}
void SavePositions(string filename)
{
var winList = GetAllWindows();
using (var sw = new StreamWriter(filename))
using (var writer = new JsonTextWriter(sw))
{
writer.Formatting = Formatting.Indented;
new JsonSerializer().Serialize(writer, winList);
}
Console.WriteLine("A list of {0} windows were written to {1}", winList.Count, filename);
Console.WriteLine("Please open it using a text editor and REMOVE any window you don't recognize.");
Console.WriteLine("For the rest, SHORTEN each Title to contain only the substring that is necessary to \n"+
"uniquely identify the window. You may also remove the size, if you don't need it.");
}
void RestorePositions(string filename)
{
var itemList = JsonConvert.DeserializeObject<List<Item>>(File.ReadAllText(filename));
var winList = GetAllWindows();
foreach (var win in winList)
{
foreach (var item in itemList)
{
if (win.Title.Contains(item.Title))
{
var v = item.GetValues();
if (v[0] == -32000) continue;
var opt = SWP_NOZORDER;
if (v[2] * v[3] <= 16) opt += SWP_NOSIZE;
if (IsIconic(win.WindowHandle))
{
ShowWindow(win.WindowHandle, 9); // SW_RESTORE = 9,
Thread.Sleep(500);
}
SetWindowPos(win.WindowHandle, IntPtr.Zero, v[0], v[1], v[2], v[3], opt);
Thread.Sleep(100);
// Not sure why but it works better with these sleeps in here.
break; // Each window is resized at most once.
}
}
}
}
List<Item> GetAllWindows()
{
var result = new List<Item>();
foreach (var process in Process.GetProcesses())
{
if (process.ProcessName == "Idle" ||
process.ProcessName == "TextInputHost")
continue;
foreach (ProcessThread thread in process.Threads)
{
var i = 1;
EnumThreadWindows(thread.Id,
(hWnd, lParam) =>
{
StringBuilder sb = new StringBuilder(MAXTITLE + 1);
GetWindowText(hWnd, sb, MAXTITLE);
var c = sb.ToString();
if (process.ProcessName == "explorer" && (sb.Length == 0 || c == "Program Manager"))
return true;
var rect = new Rect();
GetWindowRect(hWnd, ref rect);
if (rect.Right > rect.Left && rect.Bottom > rect.Top &&
!IsIconic(hWnd) && IsWindowVisible(hWnd))
{
var d = (process.MainWindowHandle == hWnd ? 1 : ++i) + "." +
process.ProcessName + "--" + c;
result.Add(new Item() { WindowHandle = hWnd, Title = d, Setup = rect.ToString() });
//Console.WriteLine("{0} {1}", d, rect.ToString()));
}
return true;
},
IntPtr.Zero);
}
}
return result;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment