Release Candidate

This commit is contained in:
Bailey Eaton 2023-05-15 14:52:40 +10:00
parent 9c048ec7fe
commit c663731818
13 changed files with 928 additions and 0 deletions

28
src/TED.sln Normal file
View File

@ -0,0 +1,28 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.2.32630.192
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TED", "TED\TED.csproj", "{BD69CE02-1B63-4949-B34B-AA6E875018DA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Open|Any CPU = Open|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BD69CE02-1B63-4949-B34B-AA6E875018DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BD69CE02-1B63-4949-B34B-AA6E875018DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BD69CE02-1B63-4949-B34B-AA6E875018DA}.Open|Any CPU.ActiveCfg = Open|Any CPU
{BD69CE02-1B63-4949-B34B-AA6E875018DA}.Open|Any CPU.Build.0 = Open|Any CPU
{BD69CE02-1B63-4949-B34B-AA6E875018DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BD69CE02-1B63-4949-B34B-AA6E875018DA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {CEDB2C21-E30F-4966-9C5F-7A936F90E393}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,33 @@
using System;
using TED.Program;
namespace TED.DrawModes
{
/// <summary>
/// Serves as the base class for different drawing modes.
/// </summary>
internal abstract class DrawModeBase
{
// The device context to be drawn on
protected IntPtr DeviceContext;
// The options to be used when drawing
protected readonly Options Options;
/// <summary>
/// Initializes a new instance of the DrawModeBase class with the specified device context and options.
/// </summary>
/// <param name="deviceContext">The device context to be drawn on.</param>
/// <param name="options">The options to be used when drawing.</param>
internal DrawModeBase(IntPtr deviceContext, Options options)
{
DeviceContext = deviceContext;
Options = options;
}
/// <summary>
/// When overridden in a derived class, performs the drawing action.
/// </summary>
public abstract void Draw();
}
}

View File

@ -0,0 +1,100 @@
using System;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using TED.Program;
using TED.Utils;
namespace TED.DrawModes
{
/// <summary>
/// Represents a drawing mode that operates once and does not update.
/// </summary>
internal class OneTimeDrawMode : DrawModeBase
{
/// <summary>
/// Initializes a new instance of the OneTimeDrawMode class with the specified device context and options.
/// </summary>
/// <param name="deviceContext">The device context to be drawn on.</param>
/// <param name="options">The options to be used when drawing.</param>
public OneTimeDrawMode(IntPtr deviceContext, Options options) : base(deviceContext, options)
{
}
/// <summary>
/// Draws the image and text on the given device context based on the options provided.
/// </summary>
public override void Draw()
{
// Calculate the luminance of the wallpaper
var wallpaperLuminance = ImageUtilities.CalculateWallpaperLuminance();
var primaryAreaRect = SystemUtilities.GetPrimaryScreenRect();
if (DeviceContext != IntPtr.Zero)
{
using (var graphics = Graphics.FromHdc(DeviceContext))
{
// Get the path of the image based on the luminance of the wallpaper
var imagePath = Options.GetImagePath(wallpaperLuminance);
// Set the text color based on the luminance of the wallpaper
var textColor = wallpaperLuminance > 0.5 ? Color.Black : Color.White;
using (var font = new Font(Options.FontName, Options.FontSize, FontStyle.Bold))
{
// Calculate the scaling factors
var scaleX = graphics.DpiX / 96.0f;
var scaleY = graphics.DpiY / 96.0f;
// Calculate the working area dimensions
var scaledWorkingAreaWidth = primaryAreaRect.X / scaleX;
var scaledWorkingAreaHeight = primaryAreaRect.Y / scaleY;
// Calculate the maximum width of all lines
var maxWidth = graphics.MeasureString(Options.Lines.Max(l => l), font).Width;
// Calculate the positions of the text and the image
var textX = scaledWorkingAreaWidth + Screen.PrimaryScreen.WorkingArea.Width - maxWidth - Options.PaddingHorizontal;
var textY = scaledWorkingAreaHeight + Screen.PrimaryScreen.WorkingArea.Height - Options.Lines.Sum(line => graphics.MeasureString(line, font).Height) - (Options.LineSpacing * (Options.Lines.Count - 1)) - Options.PaddingVertical;
if (!string.IsNullOrEmpty(imagePath))
{
using (var overlayImage = Image.FromFile(imagePath))
{
// Scale the image while maintaining its aspect ratio
ImageUtilities.ScaleImageAndMaintainAspectRatio(overlayImage.Width, overlayImage.Height, maxWidth, int.MaxValue, out int newWidth, out int newHeight);
var imageX = scaledWorkingAreaWidth + Screen.PrimaryScreen.WorkingArea.Width - newWidth - Options.PaddingHorizontal;
var imageY = scaledWorkingAreaHeight + Screen.PrimaryScreen.WorkingArea.Height - newHeight - Options.Lines.Sum(line => graphics.MeasureString(line, font).Height) - (Options.LineSpacing * (Options.Lines.Count)) - Options.PaddingVertical;
textX = imageX;
textY = imageY + newHeight + Options.LineSpacing;
// Draw the image
graphics.DrawImage(overlayImage, new RectangleF(imageX, imageY, newWidth, newHeight));
}
}
// Draw each line of text
for (var i = 0; i < Options.Lines.Count; i++)
{
var line = Options.Lines[i];
// Draw the line
graphics.DrawString(line, font, new SolidBrush(textColor), new PointF(textX, textY));
// Move the text cursor down to the next line
textY += graphics.MeasureString(line, font).Height;
// If there are more lines, add additional spacing
if (i < Options.Lines.Count)
{
textY += Options.LineSpacing;
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,91 @@
using System.Collections.Generic;
using TED.Utils;
namespace TED.Program
{
/// <summary>
/// Container for the various options utilized within TED.
/// </summary>
internal class Options
{
internal readonly int PaddingHorizontal;
internal readonly int PaddingVertical;
internal readonly int LineSpacing;
internal readonly int FontSize;
internal readonly string FontName;
internal readonly string ImagePath;
internal readonly string LightImagePath;
internal readonly string DarkImagePath;
internal readonly List<string> Lines;
internal readonly bool Debug;
internal readonly bool AdaptiveImageMode;
/// <summary>
/// Gets default options
/// </summary>
private static Options? _default;
internal static Options Default
{
get
{
_default ??= new Options(
10, 10, 8, 8, "Arial",
"", "", "",
new List<string>()
{
Tokenizer.ReplaceTokens("USERNAME: @userName"),
Tokenizer.ReplaceTokens("DEVICE NAME: @machineName"),
Tokenizer.ReplaceTokens("OS: @os"),
},
false);
return _default;
}
}
/// <summary>
/// Initializes a new instance of the Options class with the specified settings.
/// </summary>
internal Options(int paddingHorizontal, int paddingVertical,
int lineSpacing, int fontSize, string fontName,
string imagePath, string lightImagePath,
string darkImagePath, List<string> lines,
bool debug)
{
PaddingHorizontal = paddingHorizontal;
PaddingVertical = paddingVertical;
LineSpacing = lineSpacing;
FontSize = fontSize;
FontName = fontName;
ImagePath = imagePath;
LightImagePath = lightImagePath;
DarkImagePath = darkImagePath;
Lines = lines;
Debug = debug;
AdaptiveImageMode = !string.IsNullOrEmpty(LightImagePath) && !string.IsNullOrEmpty(DarkImagePath);
}
/// <summary>
/// Determines the image path based on the wallpaper's luminance.
/// When in AdaptiveImageMode, chooses the light or dark image path based on the provided luminance.
///
/// Note: AdaptiveImageMode requires both a light image and dark image to be set. If only one is provided, the regular image
/// path will be used as a fallback.
/// </summary>
/// <param name="wallpaperLuminance">The luminance of the wallpaper.</param>
/// <returns>
/// The appropriate image path considering the luminance and the AdaptiveImageMode.
/// </returns>
internal string GetImagePath(double wallpaperLuminance)
{
if (AdaptiveImageMode && !string.IsNullOrEmpty(LightImagePath) && !string.IsNullOrEmpty(DarkImagePath))
{
return wallpaperLuminance > 0.5 ? DarkImagePath : LightImagePath;
}
else
{
return ImagePath;
}
}
}
}

View File

@ -0,0 +1,159 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using TED.Utils;
namespace TED.Program
{
/// <summary>
/// The main class for the program.
/// </summary>
public class Program
{
private static Tagger? tagger;
/// <summary>
/// Application entry point
/// </summary>
/// <param name="args">An array of command-line arguments.</param>
public static void Main(string[] args)
{
tagger = new Tagger(ParseArgsIntoOptions(args));
tagger.Tag();
}
/// <summary>
/// Parses the command-line arguments into an Options object.
/// </summary>
/// <param name="args">An array of command-line arguments.</param>
/// <returns>An Options object that encapsulates application settings.</returns>
private static Options ParseArgsIntoOptions(string[] args)
{
var fontName = GetArgument(args, new string[] { "-font", "-f" }, Options.Default.FontName);
var imagePath = GetArgument(args, new string[] { "-image", "-i" }, Options.Default.ImagePath);
var darkImagePath = GetArgument(args, new string[] { "-darkimage", "-di" }, Options.Default.DarkImagePath);
var lightImagePath = GetArgument(args, new string[] { "-lightimage", "-li" }, Options.Default.LightImagePath);
var lines = Options.Default.Lines;
if (!bool.TryParse(GetArgument(args, new string[] { "-debug", "-d" }, Options.Default.Debug.ToString()), out bool debug))
{
debug = Options.Default.Debug;
}
if (!int.TryParse(GetArgument(args, new string[] { "-fontsize", "-fs" }, Options.Default.FontSize.ToString()), out int fontSize))
{
fontSize = Options.Default.FontSize;
}
if (!int.TryParse(GetArgument(args, new string[] { "-linespacing", "-ls" }, Options.Default.LineSpacing.ToString()), out int margin))
{
margin = Options.Default.LineSpacing;
}
if (!int.TryParse(GetArgument(args, new string[] { "-hpad", "-hp" }, Options.Default.PaddingHorizontal.ToString()), out int paddingHorizontal))
{
paddingHorizontal = Options.Default.PaddingHorizontal;
}
if (!int.TryParse(GetArgument(args, new string[] { "-vpad", "-vp" }, Options.Default.PaddingHorizontal.ToString()), out int paddingVertical))
{
paddingVertical = Options.Default.PaddingVertical;
}
if (args.Any(arg => arg.Contains("-line")))
{
lines.Clear();
}
for (int i = 0; i < args.Length; i++)
{
if (args[i] == "-line")
{
for (int j = i + 1; j < args.Length; j++)
{
if (args[j].StartsWith("-"))
{
break;
}
lines.Add(Tokenizer.ReplaceTokens(args[j]));
}
}
}
imagePath = string.IsNullOrEmpty(imagePath) ? imagePath : EnsureImageExists(imagePath);
lightImagePath = string.IsNullOrEmpty(lightImagePath) ? lightImagePath : EnsureImageExists(lightImagePath);
darkImagePath = string.IsNullOrEmpty(darkImagePath) ? darkImagePath : EnsureImageExists(darkImagePath);
return new Options(
paddingHorizontal,
paddingVertical,
margin,
fontSize,
fontName,
imagePath,
lightImagePath,
darkImagePath,
lines,
debug
);
}
/// <summary>
/// Validates the existence of the image file at the specified path.
/// </summary>
/// <param name="imagePath">Path of the image file.</param>
/// <returns>
/// The path of the image. If the image path points to a URL, the method attempts to download
/// the image, cache it, and then return the local path of the downloaded image.
/// </returns>
/// <exception cref="HttpRequestException">
/// Thrown if the image path points to a URL and the image fails to download.
/// </exception>
/// <exception cref="FileNotFoundException">
/// Thrown if the image path does not point to a valid local file or URL.
/// </exception>
private static string EnsureImageExists(string imagePath)
{
if (!FileUtilities.PathIsLocalFile(imagePath))
{
if (FileUtilities.PathIsUrl(imagePath))
{
var path = string.Empty;
try
{
path = FileUtilities.DownloadAndCacheFileAsync(imagePath).GetAwaiter().GetResult();
}
catch
{
throw new HttpRequestException("ERROR: Failed to acquire image from provided URL");
}
return path;
}
else
{
throw new FileNotFoundException("ERROR: Image filepath or URL is invalid.");
}
}
return imagePath;
}
/// <summary>
/// Retrieves the value of a specific command-line argument.
/// </summary>
/// <param name="args">A collection of command-line arguments.</param>
/// <param name="options">An array of options to match against the command-line arguments.</param>
/// <param name="defaultValue">The default value to return if the option is not found.</param>
/// <returns>The value of the specified command-line argument, or the default value if the option is not found.</returns>
private static string GetArgument(IEnumerable<string> args, string[] options, string defaultValue) =>
args.SkipWhile(i => !options.Contains(i)).Skip(1).Take(1).DefaultIfEmpty(defaultValue).First();
}
}

View File

@ -0,0 +1,40 @@
using TED.DrawModes;
using TED.Utils;
namespace TED.Program
{
/// <summary>
/// Provides functionality for tagging windows in the system.
/// </summary>
internal class Tagger
{
/// <summary>
/// Provides functionality for tagging windows in the system.
/// </summary>
internal Options Options;
/// <summary>
/// Initializes a new instance of the <see cref="Tagger"/> class with the specified options.
/// </summary>
/// <param name="options">The options to be used while tagging.</param>
public Tagger(Options options)
{
Options = options;
}
/// <summary>
/// Tags the window based on the given <see cref="Options"/>.
/// </summary>
public void Tag()
{
var progman = Win32Native.GetProgmanWindow();
Win32Native.ClearWindow(progman);
var workerWindow = Win32Native.CreateWorkerW(progman);
var deviceContext = Win32Native.GetWorkerWindowDeviceContext(workerWindow);
DrawModeBase drawer = new OneTimeDrawMode(deviceContext, Options);
drawer.Draw();
Win32Native.ReleaseWorkerWindowDeviceContext(workerWindow, deviceContext);
}
}
}

View File

@ -0,0 +1,77 @@
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace TED.Utils
{
/// <summary>
/// Provides utility methods for working with files
/// </summary>
internal class FileUtilities
{
/// <summary>
/// Downloads a file from a specified URL and saves it to a cache.
/// If the file already exists in the cache, the function returns the path to the cached file.
/// If the file does not exist in the cache, the function downloads the file, saves it to the cache, and then returns the path to the cached file.
/// </summary>
/// <param name="url">The URL of the file to download.</param>
/// <returns>A task that represents the asynchronous operation. The task result is the path to the downloaded (or cached) file.</returns>
public static async Task<string> DownloadAndCacheFileAsync(string url)
{
using (var client = new HttpClient()
{
Timeout = TimeSpan.FromSeconds(10)
})
{
var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
var etag = response.Headers.ETag?.Tag.Replace("\"", string.Empty) ?? "untagged";
if (!Directory.Exists(Path.Combine(Path.GetTempPath(), "TED")))
{
Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "TED"));
}
var path = Path.Combine(Path.GetTempPath(), "TED", $"{etag}.png");
if (File.Exists(path))
{
return path;
}
var filesToDelete = Directory.GetFiles(Path.Combine(Path.GetTempPath(), "TED"), "*.png")
.Where(filePath => Path.GetFileNameWithoutExtension(filePath) != etag);
foreach (var fileToDelete in filesToDelete)
{
File.Delete(fileToDelete);
}
using (var stream = await response.Content.ReadAsStreamAsync())
{
using (var fileStream = new FileStream(path, FileMode.CreateNew))
{
await stream.CopyToAsync(fileStream);
return path;
}
}
}
}
/// <summary>
/// Downloads a file from a specified URL and saves it to a cache.
/// If the file already exists in the cache, the function returns the path to the cached file.
/// If the file does not exist in the cache, the function downloads the file, saves it to the cache, and then returns the path to the cached file.
/// </summary>
/// <param name="url">The URL of the file to download.</param>
/// <returns>A task that represents the asynchronous operation. The task result is the path to the downloaded (or cached) file.</returns>
public static bool PathIsLocalFile(string path) => Path.IsPathFullyQualified(path) && File.Exists(path);
/// <summary>
/// Determines whether a specified path represents a URL.
/// </summary>
/// <param name="path">The path to check.</param>
/// <returns>True if the path represents a URL; otherwise, false.</returns>
public static bool PathIsUrl(string path) => !Path.IsPathFullyQualified(path) && Uri.TryCreate(path, UriKind.Absolute, out _);
}
}

View File

@ -0,0 +1,88 @@
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
namespace TED.Utils
{
/// <summary>
/// Provides utility methods for working with images
/// </summary>
public class ImageUtilities
{
/// <summary>
/// Scales the image dimensions to fit within the specified maximum width and height, maintaining the original aspect ratio.
/// </summary>
/// <param name="srcWidth">The original image width.</param>
/// <param name="srcHeight">The original image height.</param>
/// <param name="maxWidth">The maximum allowable width for the scaled image.</param>
/// <param name="maxHeight">The maximum allowable height for the scaled image.</param>
/// <param name="destWidth">The calculated width for the scaled image.</param>
/// <param name="destHeight">The calculated height for the scaled image.</param>
public static void ScaleImageAndMaintainAspectRatio(int srcWidth, int srcHeight, float maxWidth, float maxHeight, out int destWidth, out int destHeight)
{
var ratioX = (double)maxWidth / srcWidth;
var ratioY = (double)maxHeight / srcHeight;
var ratio = Math.Min(ratioX, ratioY);
destWidth = (int)(srcWidth * ratio);
destHeight = (int)(srcHeight * ratio);
}
/// <summary>
/// Calculates the perceived luminance of the current desktop wallpaper.
/// </summary>
/// <returns>A value representing the calculated perceived luminance of the wallpaper. Returns 0.0 if the wallpaper path is not found.</returns>
public static double CalculateWallpaperLuminance()
{
// Get the wallpaper path from the registry
string wallpaperPath = SystemUtilities.GetWallpaperPathFromRegistry();
double luminance = 0.0;
if (!string.IsNullOrEmpty(wallpaperPath))
{
using (var bmp = new Bitmap(wallpaperPath))
{
luminance = CalculateImageLuminance01(bmp);
}
}
return luminance;
}
/// <summary>
/// Calculates the perceived luminance of a Bitmap image.
/// </summary>
/// <param name="bm">The Bitmap image to calculate the luminance for.</param>
/// <returns>A value representing the calculated luminance of the image.</returns>
public static double CalculateImageLuminance01(Bitmap bm)
{
var lum = 0.0;
var width = bm.Width;
var height = bm.Height;
var bytesPerPixel = Image.GetPixelFormatSize(bm.PixelFormat) / 8;
var srcData = bm.LockBits(new Rectangle(0, 0, bm.Width, bm.Height), ImageLockMode.ReadOnly, bm.PixelFormat);
var stride = srcData.Stride;
IntPtr scan0 = srcData.Scan0;
// Luminance (standard, objective): (0.2126*R) + (0.7152*G) + (0.0722*B)
// Luminance (perceived option 1): (0.299*R + 0.587*G + 0.114*B)
// Luminance (perceived option 2, slower to calculate): sqrt( 0.299*R^2 + 0.587*G^2 + 0.114*B^2 )
for (var y = 0; y < height; y++)
{
for (var x = 0; x < width; x++)
{
var idx = y * stride + x * bytesPerPixel;
lum += (0.299 * Marshal.ReadByte(scan0, idx + 2) + 0.587 * Marshal.ReadByte(scan0, idx + 1) + 0.114 * Marshal.ReadByte(scan0, idx)) / 255.0;
}
}
bm.UnlockBits(srcData);
var normalized = lum / (width * height);
return normalized;
}
}
}

View File

@ -0,0 +1,45 @@
using Microsoft.Win32;
using System.Drawing;
using System.Windows.Forms;
namespace TED.Utils
{
/// <summary>
/// Provides utility functions related to system configurations.
/// </summary>
public class SystemUtilities
{
/// <summary>
/// Get's the Rectangle of the Primary Screen in Virtual Screen space.
/// </summary>
/// <returns>The Rectangle of the Primary Screen in Virtual Screen space.</returns>
public static Rectangle GetPrimaryScreenRect()
{
return new Rectangle(
Screen.PrimaryScreen.Bounds.X - SystemInformation.VirtualScreen.Left,
Screen.PrimaryScreen.Bounds.Y - SystemInformation.VirtualScreen.Top,
Screen.PrimaryScreen.WorkingArea.Width,
Screen.PrimaryScreen.WorkingArea.Height
);
}
/// <summary>
/// Fetches the current wallpaper path from the system registry.
/// </summary>
/// <returns>The full path of the current desktop wallpaper.</returns>
public static string GetWallpaperPathFromRegistry()
{
string result = string.Empty;
using (var key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Desktop"))
{
if (key != null)
{
result = key.GetValue("Wallpaper").ToString();
key.Close();
}
}
return result;
}
}
}

View File

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Security.Principal;
namespace TED.Utils
{
/// <summary>
/// Provides functionality for replacing tokens in a string.
/// </summary>
internal class Tokenizer
{
/// <summary>
/// A dictionary that maps token names to their corresponding values.
/// </summary>
static Dictionary<string, Func<string>> TokenLookup = new Dictionary<string, Func<string>>()
{
{ "@userName", () => WindowsIdentity.GetCurrent().Name },
{ "@machineName", () => Environment.MachineName },
{ "@os", () => Environment.OSVersion.ToString() },
};
/// <summary>
/// Replaces tokens in the input string with their corresponding values.
/// </summary>
/// <param name="input">The string that may contain tokens to be replaced.</param>
/// <returns>The input string with all tokens replaced by their corresponding values.</returns>
public static string ReplaceTokens(string input)
{
foreach (var kvp in TokenLookup)
{
var token = kvp.Key;
var value = kvp.Value();
input = input.Replace(token, value);
}
return input;
}
}
}

View File

@ -0,0 +1,169 @@
using System;
using System.Runtime.InteropServices;
namespace TED.Utils
{
/// <summary>
///
/// The information and methods stored within this class are largely attributed to <see href="https://www.pinvoke.net/"/>
/// It has been a fantastic resource for tinkering with and learning how native methods work.
///
/// </summary>
public class Win32Native
{
public delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam);
[Flags()]
public enum DeviceContextValues : uint
{
/// <summary>DCX_WINDOW: Returns a DC that corresponds to the window rectangle rather
/// than the client rectangle.</summary>
Window = 0x00000001,
/// <summary>DCX_CACHE: Returns a DC from the cache, rather than the OWNDC or CLASSDC
/// window. Essentially overrides CS_OWNDC and CS_CLASSDC.</summary>
Cache = 0x00000002,
/// <summary>DCX_NORESETATTRS: Does not reset the attributes of this DC to the
/// default attributes when this DC is released.</summary>
NoResetAttrs = 0x00000004,
/// <summary>DCX_CLIPCHILDREN: Excludes the visible regions of all child windows
/// below the window identified by hWnd.</summary>
ClipChildren = 0x00000008,
/// <summary>DCX_CLIPSIBLINGS: Excludes the visible regions of all sibling windows
/// above the window identified by hWnd.</summary>
ClipSiblings = 0x00000010,
/// <summary>DCX_PARENTCLIP: Uses the visible region of the parent window. The
/// parent's WS_CLIPCHILDREN and CS_PARENTDC style bits are ignored. The origin is
/// set to the upper-left corner of the window identified by hWnd.</summary>
ParentClip = 0x00000020,
/// <summary>DCX_EXCLUDERGN: The clipping region identified by hrgnClip is excluded
/// from the visible region of the returned DC.</summary>
ExcludeRgn = 0x00000040,
/// <summary>DCX_INTERSECTRGN: The clipping region identified by hrgnClip is
/// intersected with the visible region of the returned DC.</summary>
IntersectRgn = 0x00000080,
/// <summary>DCX_EXCLUDEUPDATE: Unknown...Undocumented</summary>
ExcludeUpdate = 0x00000100,
/// <summary>DCX_INTERSECTUPDATE: Unknown...Undocumented</summary>
IntersectUpdate = 0x00000200,
/// <summary>DCX_LOCKWINDOWUPDATE: Allows drawing even if there is a LockWindowUpdate
/// call in effect that would otherwise exclude this window. Used for drawing during
/// tracking.</summary>
LockWindowUpdate = 0x00000400,
/// <summary>DCX_USESTYLE: Undocumented, something related to WM_NCPAINT message.</summary>
UseStyle = 0x00010000,
/// <summary>DCX_VALIDATE When specified with DCX_INTERSECTUPDATE, causes the DC to
/// be completely validated. Using this function with both DCX_INTERSECTUPDATE and
/// DCX_VALIDATE is identical to using the BeginPaint function.</summary>
Validate = 0x00200000,
}
[Flags]
public enum SendMessageTimeoutFlags : uint
{
SMTO_NORMAL = 0x0,
SMTO_BLOCK = 0x1,
SMTO_ABORTIFHUNG = 0x2,
SMTO_NOTIMEOUTIFNOTHUNG = 0x8,
SMTO_ERRORONEXIT = 0x20
}
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
//Keep
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr childAfter, string className, IntPtr windowTitle);
[DllImport("user32.dll")]
public static extern IntPtr GetDCEx(IntPtr hWnd, IntPtr hrgnClip, DeviceContextValues flags);
[DllImport("user32.dll", EntryPoint = "ReleaseDC")]
public static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDc);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, int wParam, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern IntPtr SendMessageTimeout(IntPtr windowHandle, uint Msg, IntPtr wParam, IntPtr lParam, SendMessageTimeoutFlags flags, uint timeout, out IntPtr result);
public static IntPtr GetProgmanWindow()
{
// We're going to render the watermark over the wallpaper, but behind desktop icons.
// We can do so with some trickery using the Progman window
// We're going to spawn a WorkerW window behind the desktop icons, and then render our watermark to that
return FindWindow("Progman", null);
}
public static void ClearWindow(IntPtr windowHandle)
{
// The below command clears any existing watermark we've drawn
// The Thread.Sleep() is necessary because the command has a minor delay to it
// Without it, the clear will end up running after we've generated a watermark,
// removing our newly generated watermark
SendMessage(windowHandle, 0x0034, 4, IntPtr.Zero);
//Thread.Sleep(2000);
}
public static IntPtr CreateWorkerW(IntPtr progman)
{
IntPtr result = IntPtr.Zero;
// Send 0x052C to Progman. This message directs Progman to spawn a
// WorkerW behind the desktop icons. If it is already there, nothing
// happens.
SendMessageTimeout(progman,
0x052C,
new IntPtr(0),
IntPtr.Zero,
SendMessageTimeoutFlags.SMTO_NORMAL,
1000,
out result);
IntPtr workerw = IntPtr.Zero;
// We enumerate all Windows, until we find one, that has the SHELLDLL_DefView
// as a child. These should be our desktop icons.
// If we found that window, we take its next sibling and assign it to workerw.
EnumWindows(new EnumWindowsProc((tophandle, topparamhandle) =>
{
IntPtr p = FindWindowEx(tophandle,
IntPtr.Zero,
"SHELLDLL_DefView",
IntPtr.Zero);
if (p != IntPtr.Zero)
{
// Gets the WorkerW Window after the current one.
workerw = FindWindowEx(IntPtr.Zero,
tophandle,
"WorkerW",
IntPtr.Zero);
}
return true;
}), IntPtr.Zero);
return workerw;
}
public static IntPtr GetWorkerWindowDeviceContext(IntPtr workerw) => GetDCEx(workerw, IntPtr.Zero, (DeviceContextValues)0x403);
public static void ReleaseWorkerWindowDeviceContext(IntPtr workerw, IntPtr deviceContext) => ReleaseDC(workerw, deviceContext);
}
}

40
src/TED/TED.csproj Normal file
View File

@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0-windows10.0.22621.0</TargetFramework>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishReadyToRun>true</PublishReadyToRun>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>Link</TrimMode>
<RootNamespace>TED</RootNamespace>
<Nullable>enable</Nullable>
<Configurations>Debug;Release;Open</Configurations>
<Version>1.0.4.1-RC1</Version>
<InformationalVersion>Prerelease</InformationalVersion>
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<ApplicationManifest>app.manifest</ApplicationManifest>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<Company>Health IT</Company>
<Title>TED - Tag Every Desktop</Title>
<Authors>Health IT</Authors>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Open|AnyCPU'">
<Optimize>True</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<Optimize>False</Optimize>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.WindowsDesktop.App.WindowsForms" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\PublishProfiles\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
</ItemGroup>
</Project>

17
src/TED/app.manifest Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</windowsSettings>
</application>
</assembly>