core/AsciiFX.Desktop.cs
|
using System;
using System.Diagnostics; using System.Drawing; using System.Drawing.Text; using System.Runtime.InteropServices; using System.Text; using System.Windows.Forms; namespace AsciiFX { // Shows an animation on the desktop: above the wallpaper (static or live), below every normal window, // with a see-through background and clicks passing straight through. Windows only. public class DesktopPlayer : Form { [DllImport("user32.dll")] static extern bool SetProcessDPIAware(); [DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern IntPtr FindWindowEx(IntPtr parent, IntPtr after, string cls, string title); [DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetWindowText(IntPtr hwnd, StringBuilder text, int max); [DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr hwnd); [DllImport("user32.dll")] static extern IntPtr GetWindow(IntPtr hwnd, uint cmd); [DllImport("user32.dll")] static extern bool SetWindowPos(IntPtr hwnd, IntPtr after, int x, int y, int cx, int cy, uint flags); const string TitlePrefix = "AsciiFX "; const int MaxBrushes = 8; // nearby colors share a brush, so each row needs only a few text draws readonly IAnimation anim; readonly IBackgroundAnimation withBackgrounds; readonly int cols, rows; readonly char[] chars; readonly int[] colors, backgrounds; readonly char[] line; readonly int[] group; // palette index -> brush readonly SolidBrush[] brushes; readonly SolidBrush[] exact; // palette index -> its exact color, for filled blocks and backgrounds readonly SolidBrush outlineBrush; readonly bool outline; readonly Font font; readonly float cellW, cellH; readonly Stopwatch clock = Stopwatch.StartNew(); int ticks; // layout "box": a cols x rows window placed by position/x/y. // layout "sky": the full screen width and the top heightPercent of the screen (100 = all of it). public static void Run(IAnimation anim, string name, float fontSize, string layout, int cols, int rows, int heightPercent, bool outline, string position, int x, int y) { SetProcessDPIAware(); // Size the font for the screen's scaling and measure one monospace cell Font font; float cellW; using (Graphics g = Graphics.FromHwnd(IntPtr.Zero)) { font = new Font("Consolas", fontSize * g.DpiX / 96f, GraphicsUnit.Pixel); g.TextRenderingHint = TextRenderingHint.SingleBitPerPixelGridFit; cellW = g.MeasureString(new string('M', 20), font, PointF.Empty, StringFormat.GenericTypographic).Width / 20; } float cellH = (float)(cellW * Cell.Aspect); if (layout == "sky") { Rectangle screen = Screen.PrimaryScreen.Bounds; cols = (int)((screen.Width - 4) / cellW); rows = (int)(screen.Height * Math.Max(1, Math.Min(100, heightPercent)) / 100.0 / cellH); position = "sky"; } Application.Run(new DesktopPlayer(anim, name, font, cellW, cellH, cols, rows, outline, position, x, y)); } DesktopPlayer(IAnimation anim, string name, Font font, float cellW, float cellH, int cols, int rows, bool outline, string position, int x, int y) { this.anim = anim; this.font = font; this.cellW = cellW; this.cellH = cellH; this.cols = Math.Max(1, cols); this.rows = Math.Max(1, rows); this.outline = outline; withBackgrounds = anim as IBackgroundAnimation; chars = new char[this.cols * this.rows]; colors = new int[this.cols * this.rows]; backgrounds = new int[this.cols * this.rows]; line = new char[this.cols]; anim.Resize(this.cols, this.rows); Text = TitlePrefix + name; FormBorderStyle = FormBorderStyle.None; ShowInTaskbar = false; StartPosition = FormStartPosition.Manual; BackColor = Color.FromArgb(1, 0, 1); // keyed out, so only the characters show TransparencyKey = BackColor; DoubleBuffered = true; group = GroupColors(anim.Palette, out brushes); exact = new SolidBrush[anim.Palette.Length]; for (int i = 0; i < exact.Length; i++) exact[i] = new SolidBrush(Color.FromArgb((anim.Palette[i] >> 16) & 255, (anim.Palette[i] >> 8) & 255, anim.Palette[i] & 255)); outlineBrush = new SolidBrush(Color.FromArgb(17, 17, 27)); ClientSize = new Size((int)(this.cols * cellW) + 4, (int)(this.rows * cellH) + 4); // The sky starts at the very top of the screen, so at 100% it covers all of it Location = position == "sky" ? Screen.PrimaryScreen.Bounds.Location : PlaceOn(Screen.PrimaryScreen.WorkingArea, position, x, y); Timer timer = new Timer(); timer.Interval = Math.Max(15, anim.FrameMs); timer.Tick += delegate { if (ticks++ % 15 == 0) KeepOnDesktop(); Invalidate(); }; timer.Start(); } // Up to MaxBrushes representative colors from the palette; every palette color uses the closest one static int[] GroupColors(int[] palette, out SolidBrush[] brushes) { int count = Math.Min(MaxBrushes, palette.Length); int[] reps = new int[count]; for (int i = 0; i < count; i++) reps[i] = palette[count == 1 ? 0 : i * (palette.Length - 1) / (count - 1)]; int[] group = new int[palette.Length]; for (int p = 0; p < palette.Length; p++) { int best = 0, bestDist = int.MaxValue; for (int r = 0; r < count; r++) { int dr = ((palette[p] >> 16) & 255) - ((reps[r] >> 16) & 255); int dg = ((palette[p] >> 8) & 255) - ((reps[r] >> 8) & 255); int db = (palette[p] & 255) - (reps[r] & 255); int dist = dr * dr + dg * dg + db * db; if (dist < bestDist) { bestDist = dist; best = r; } } group[p] = best; } brushes = new SolidBrush[count]; for (int r = 0; r < count; r++) brushes[r] = new SolidBrush(Color.FromArgb((reps[r] >> 16) & 255, (reps[r] >> 8) & 255, reps[r] & 255)); return group; } // A named spot ("center", "top-left", "bottom", ...), or exact pixels from the top-left when x/y are given Point PlaceOn(Rectangle area, string position, int x, int y) { const int margin = 40; string p = (position ?? "center").ToLowerInvariant(); int left = area.Left + (area.Width - Width) / 2, top = area.Top + (area.Height - Height) / 2; if (p.Contains("left")) left = area.Left + margin; if (p.Contains("right")) left = area.Right - Width - margin; if (p.Contains("top")) top = area.Top + margin; if (p.Contains("bottom")) top = area.Bottom - Height - margin; if (x >= 0) left = area.Left + x; if (y >= 0) top = area.Top + y; return new Point(left, top); } // Tool window (no taskbar/Alt+Tab), layered + transparent (clicks pass through), never takes focus protected override CreateParams CreateParams { get { CreateParams cp = base.CreateParams; cp.ExStyle |= 0x80 | 0x20 | 0x80000 | 0x08000000; return cp; } } protected override bool ShowWithoutActivation { get { return true; } } protected override void OnPaint(PaintEventArgs e) { if (withBackgrounds != null) withBackgrounds.Frame(clock.Elapsed.TotalSeconds, chars, colors, backgrounds); else { anim.Frame(clock.Elapsed.TotalSeconds, chars, colors); for (int k = 0; k < backgrounds.Length; k++) backgrounds[k] = -1; } Graphics g = e.Graphics; g.TextRenderingHint = TextRenderingHint.SingleBitPerPixelGridFit; // no antialiasing, so no colored fringes for (int y = 0; y < rows; y++) { float top = 2 + y * cellH; int row = y * cols; FillBlocks(g, y); for (int x = 0; x < cols; x++) line[x] = colors[row + x] < 0 ? ' ' : chars[row + x]; // A dark copy offset by a pixel keeps the characters readable on bright wallpapers if (outline) g.DrawString(new string(line), font, outlineBrush, 3, top + 1, StringFormat.GenericTypographic); // Then each run of same-brush characters in one go int start = 0; while (start < cols) { int b = colors[row + start] < 0 ? -1 : group[colors[row + start]], end = start + 1; while (end < cols && (colors[row + end] < 0 ? -1 : group[colors[row + end]]) == b) end++; if (b >= 0) g.DrawString(new string(line, start, end - start), font, brushes[b], 2 + start * cellW, top, StringFormat.GenericTypographic); start = end; } } } // Backgrounds, and the block characters '▀' '▄' '█', are painted as exact rectangles rather than text, so // pictures come out crisp in any font. Those cells are then cleared so the text pass skips them. void FillBlocks(Graphics g, int y) { int row = y * cols; int top = (int)(2 + y * cellH), middle = (int)(2 + (y + 0.5f) * cellH), bottom = (int)(2 + (y + 1) * cellH); for (int x = 0; x < cols; x++) { int k = row + x; char c = chars[k]; bool upper = c == '▀' || c == '█', lower = c == '▄' || c == '█'; if (backgrounds[k] < 0 && !(colors[k] >= 0 && (upper || lower))) continue; int left = (int)(2 + x * cellW), right = (int)(2 + (x + 1) * cellW); int topColor = colors[k] >= 0 && upper ? colors[k] : backgrounds[k]; int bottomColor = colors[k] >= 0 && lower ? colors[k] : backgrounds[k]; if (topColor >= 0) g.FillRectangle(exact[topColor], left, top, right - left, middle - top); if (bottomColor >= 0) g.FillRectangle(exact[bottomColor], left, middle, right - left, bottom - middle); if (upper || lower) colors[k] = -1; // fully painted already } } // Stay just above the window that holds the desktop icons and wallpaper, below everything else. // Other AsciiFX windows share that layer, so only move when a normal window got in between. void KeepOnDesktop() { IntPtr host = FindWindowEx(IntPtr.Zero, IntPtr.Zero, "Progman", null); if (FindWindowEx(host, IntPtr.Zero, "SHELLDLL_DefView", null) == IntPtr.Zero) { IntPtr worker = IntPtr.Zero; while ((worker = FindWindowEx(IntPtr.Zero, worker, "WorkerW", null)) != IntPtr.Zero) if (FindWindowEx(worker, IntPtr.Zero, "SHELLDLL_DefView", null) != IntPtr.Zero) { host = worker; break; } } StringBuilder text = new StringBuilder(64); for (IntPtr w = GetWindow(host, 3); w != IntPtr.Zero; w = GetWindow(w, 3)) // GW_HWNDPREV: walk upward { if (w == Handle) return; text.Length = 0; GetWindowText(w, text, 64); if (IsWindowVisible(w) && !text.ToString().StartsWith(TitlePrefix)) break; } IntPtr above = GetWindow(host, 3); if (above == IntPtr.Zero) return; SetWindowPos(Handle, above, 0, 0, 0, 0, 0x0001 | 0x0002 | 0x0010); // NOSIZE | NOMOVE | NOACTIVATE } } } |