From 44c51398b89a4fd10c3dd16435c557e55928f4cd Mon Sep 17 00:00:00 2001 From: Florian Date: Thu, 3 Sep 2026 21:48:28 +0200 Subject: [PATCH] Initial commit: application ColorPicker (palette, pipette ecran, couleur de dossier) --- .gitignore | 3 + LISEZMOI.txt | 45 ++++++ app.manifest | 26 +++ build.bat | 18 +++ src/FolderColorizer.cs | 189 ++++++++++++++++++++++ src/IconWriter.cs | 52 ++++++ src/MagnifierForm.cs | 69 ++++++++ src/MainForm.cs | 302 +++++++++++++++++++++++++++++++++++ src/Palette.cs | 62 +++++++ src/Program.cs | 16 ++ src/ScreenColorPickerForm.cs | 94 +++++++++++ tests/IconPreview.cs | 34 ++++ tests/TestHarness.cs | 46 ++++++ 13 files changed, 956 insertions(+) create mode 100644 .gitignore create mode 100644 LISEZMOI.txt create mode 100644 app.manifest create mode 100644 build.bat create mode 100644 src/FolderColorizer.cs create mode 100644 src/IconWriter.cs create mode 100644 src/MagnifierForm.cs create mode 100644 src/MainForm.cs create mode 100644 src/Palette.cs create mode 100644 src/Program.cs create mode 100644 src/ScreenColorPickerForm.cs create mode 100644 tests/IconPreview.cs create mode 100644 tests/TestHarness.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..490f83c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.exe +*.pdb +*.obj diff --git a/LISEZMOI.txt b/LISEZMOI.txt new file mode 100644 index 0000000..91710e5 --- /dev/null +++ b/LISEZMOI.txt @@ -0,0 +1,45 @@ +ColorPicker - Palette de couleurs & couleur de dossier +======================================================== + +UTILISATION +----------- +Aucune installation requise : lancez simplement ColorPicker.exe. +Compatible Windows 10 et 11, 32 et 64 bits (necessite le .NET Framework 4.8, +present par defaut sur ces versions de Windows). + +FONCTIONNALITES +---------------- +1. Palette de couleurs complete (a gauche) : cliquez sur une case pour la + selectionner. + +2. Couleur personnalisee / en dehors de la palette : + - Bouton "Preleve une couleur a l'ecran" : le curseur se transforme en + pipette, deplacez-le n'importe ou sur l'ecran (meme en dehors de + l'application) et cliquez pour capturer la couleur exacte du pixel + survole. Echap pour annuler. + - Bouton "Couleur personnalisee..." : ouvre le selecteur Windows complet + (roue de couleurs / valeurs RVB ou TSL precises). + - Vous pouvez aussi taper directement un code hexadecimal dans le champ + prevu puis valider avec Entree ou le bouton "OK". + +3. Codes en temps reel : le code hexadecimal (#RRGGBB) et le code RGB + (R, G, B) s'affichent et se mettent a jour instantanement. Chaque champ + a son propre bouton "Copier" pour coller le code dans PowerShell, cmd, + Word, Excel, ou tout autre logiciel. + +4. Couleur d'un dossier Windows : + - "Parcourir..." pour choisir le dossier a colorer. + - "Appliquer la couleur au dossier" applique la couleur actuellement + selectionnee a l'icone du dossier (icone generee et un fichier + desktop.ini sont crees dans le dossier, en fichiers caches/systeme). + - "Restaurer la couleur par defaut" supprime ces fichiers et redonne au + dossier son icone Windows standard. + - Si l'icone ne se met pas a jour immediatement dans l'Explorateur, + fermez puis rouvrez la fenetre du dossier (ou appuyez sur F5). + +RECOMPILER LE PROJET +--------------------- +Le code source complet se trouve dans le dossier src\. Pour recompiler, +executez build.bat (utilise le compilateur C# fourni avec le +.NET Framework, deja present sur Windows - aucun outil supplementaire +n'est necessaire). diff --git a/app.manifest b/app.manifest new file mode 100644 index 0000000..8469a61 --- /dev/null +++ b/app.manifest @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + true/pm + PerMonitorV2, PerMonitor, System + + + diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..27b47d4 --- /dev/null +++ b/build.bat @@ -0,0 +1,18 @@ +@echo off +setlocal +set CSC="%WINDIR%\Microsoft.NET\Framework64\v4.0.30319\csc.exe" +if not exist %CSC% set CSC="%WINDIR%\Microsoft.NET\Framework\v4.0.30319\csc.exe" + +%CSC% /nologo /platform:anycpu /target:winexe /out:ColorPicker.exe /win32manifest:app.manifest ^ + /reference:System.dll /reference:System.Core.dll /reference:System.Drawing.dll /reference:System.Windows.Forms.dll ^ + src\Program.cs src\MainForm.cs src\Palette.cs src\ScreenColorPickerForm.cs src\MagnifierForm.cs src\FolderColorizer.cs src\IconWriter.cs + +if %ERRORLEVEL% NEQ 0 ( + echo. + echo *** Echec de la compilation *** + exit /b 1 +) + +echo. +echo Compilation reussie : ColorPicker.exe +endlocal diff --git a/src/FolderColorizer.cs b/src/FolderColorizer.cs new file mode 100644 index 0000000..5afed6a --- /dev/null +++ b/src/FolderColorizer.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.IO; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace ColorPickerApp +{ + /// Applique ou restaure une icone de dossier coloree via desktop.ini (aucune API Windows + /// ne permet de "teinter" nativement l'icone systeme : on genere une icone de remplacement). + internal static class FolderColorizer + { + private const string IconFileName = ".colorpicker_folder.ico"; + private const string BackupSuffix = ".colorpicker_backup"; + + [DllImport("shell32.dll")] + private static extern void SHChangeNotify(int wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2); + + private const int SHCNE_ASSOCCHANGED = 0x08000000; + private const int SHCNF_IDLIST = 0x0000; + + public static void ApplyColor(string folderPath, Color color) + { + if (!Directory.Exists(folderPath)) + throw new DirectoryNotFoundException(folderPath); + + string iconPath = Path.Combine(folderPath, IconFileName); + string desktopIniPath = Path.Combine(folderPath, "desktop.ini"); + + var images = new List + { + DrawFolderIcon(color, 16), + DrawFolderIcon(color, 32), + DrawFolderIcon(color, 48), + DrawFolderIcon(color, 256) + }; + + ClearReadOnlyAndHidden(iconPath); + IconWriter.WriteIcon(iconPath, images); + foreach (var b in images) b.Dispose(); + File.SetAttributes(iconPath, FileAttributes.Hidden | FileAttributes.System); + + // Conserver un desktop.ini existant (personnalise par l'utilisateur) si present et non genere par nous + if (File.Exists(desktopIniPath) && !IsOurDesktopIni(desktopIniPath)) + { + File.Copy(desktopIniPath, desktopIniPath + BackupSuffix, true); + } + + ClearReadOnlyAndHidden(desktopIniPath); + File.WriteAllText(desktopIniPath, + "[.ShellClassInfo]" + Environment.NewLine + + "IconResource=" + IconFileName + ",0" + Environment.NewLine + + "IconFile=" + IconFileName + Environment.NewLine + + "IconIndex=0" + Environment.NewLine + + "; genere par ColorPicker" + Environment.NewLine); + File.SetAttributes(desktopIniPath, FileAttributes.Hidden | FileAttributes.System); + + var dirInfo = new DirectoryInfo(folderPath); + dirInfo.Attributes |= FileAttributes.ReadOnly; + + NotifyShell(folderPath); + } + + public static void RestoreDefault(string folderPath) + { + if (!Directory.Exists(folderPath)) + throw new DirectoryNotFoundException(folderPath); + + string iconPath = Path.Combine(folderPath, IconFileName); + string desktopIniPath = Path.Combine(folderPath, "desktop.ini"); + string backupPath = desktopIniPath + BackupSuffix; + + if (File.Exists(iconPath)) + { + ClearReadOnlyAndHidden(iconPath); + File.Delete(iconPath); + } + + if (File.Exists(desktopIniPath) && IsOurDesktopIni(desktopIniPath)) + { + ClearReadOnlyAndHidden(desktopIniPath); + File.Delete(desktopIniPath); + + if (File.Exists(backupPath)) + { + File.Copy(backupPath, desktopIniPath, true); + File.SetAttributes(desktopIniPath, FileAttributes.Hidden | FileAttributes.System); + File.Delete(backupPath); + } + } + + var dirInfo = new DirectoryInfo(folderPath); + dirInfo.Attributes &= ~FileAttributes.ReadOnly; + + NotifyShell(folderPath); + } + + public static bool HasCustomColor(string folderPath) + { + string desktopIniPath = Path.Combine(folderPath, "desktop.ini"); + return File.Exists(desktopIniPath) && IsOurDesktopIni(desktopIniPath); + } + + private static bool IsOurDesktopIni(string desktopIniPath) + { + try + { + string content = File.ReadAllText(desktopIniPath); + return content.IndexOf(IconFileName, StringComparison.OrdinalIgnoreCase) >= 0; + } + catch + { + return false; + } + } + + private static void ClearReadOnlyAndHidden(string filePath) + { + if (File.Exists(filePath)) + { + File.SetAttributes(filePath, FileAttributes.Normal); + } + } + + private static void NotifyShell(string folderPath) + { + SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, IntPtr.Zero, IntPtr.Zero); + } + + private static Bitmap DrawFolderIcon(Color color, int size) + { + var bmp = new Bitmap(size, size, System.Drawing.Imaging.PixelFormat.Format32bppArgb); + using (var g = Graphics.FromImage(bmp)) + { + g.SmoothingMode = SmoothingMode.AntiAlias; + g.PixelOffsetMode = PixelOffsetMode.HighQuality; + g.Clear(Color.Transparent); + + float w = size, h = size; + Color body = color; + Color tab = ControlPaint.Light(color, 0.15f); + Color outline = ControlPaint.Dark(color, 0.25f); + + float tabW = w * 0.45f, tabH = h * 0.18f; + float tabX = w * 0.06f, tabY = h * 0.18f; + var tabRect = new RectangleF(tabX, tabY, tabW, tabH); + + float bodyX = w * 0.06f, bodyY = h * 0.30f; + float bodyW = w * 0.88f, bodyH = h * 0.56f; + var bodyRect = new RectangleF(bodyX, bodyY, bodyW, bodyH); + + float radius = Math.Max(1f, size * 0.06f); + + using (var tabPath = RoundedRect(tabRect, radius)) + using (var tabBrush = new SolidBrush(tab)) + { + g.FillPath(tabBrush, tabPath); + } + + using (var bodyPath = RoundedRect(bodyRect, radius)) + { + using (var bodyBrush = new LinearGradientBrush(bodyRect, ControlPaint.Light(body, 0.05f), ControlPaint.Dark(body, 0.05f), LinearGradientMode.Vertical)) + { + g.FillPath(bodyBrush, bodyPath); + } + using (var pen = new Pen(outline, Math.Max(1f, size / 32f))) + { + g.DrawPath(pen, bodyPath); + } + } + } + return bmp; + } + + private static GraphicsPath RoundedRect(RectangleF rect, float radius) + { + var path = new GraphicsPath(); + float d = radius * 2; + path.AddArc(rect.X, rect.Y, d, d, 180, 90); + path.AddArc(rect.Right - d, rect.Y, d, d, 270, 90); + path.AddArc(rect.Right - d, rect.Bottom - d, d, d, 0, 90); + path.AddArc(rect.X, rect.Bottom - d, d, d, 90, 90); + path.CloseFigure(); + return path; + } + } +} diff --git a/src/IconWriter.cs b/src/IconWriter.cs new file mode 100644 index 0000000..b612afc --- /dev/null +++ b/src/IconWriter.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; + +namespace ColorPickerApp +{ + /// Ecrit un fichier .ico multi-resolution a partir de bitmaps PNG (format ICO "Vista"). + internal static class IconWriter + { + public static void WriteIcon(string path, IList images) + { + using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write)) + using (var writer = new BinaryWriter(fs)) + { + writer.Write((ushort)0); // reserved + writer.Write((ushort)1); // type = icon + writer.Write((ushort)images.Count); + + var pngBlocks = new byte[images.Count][]; + for (int i = 0; i < images.Count; i++) + { + using (var ms = new MemoryStream()) + { + images[i].Save(ms, ImageFormat.Png); + pngBlocks[i] = ms.ToArray(); + } + } + + int offset = 6 + images.Count * 16; + for (int i = 0; i < images.Count; i++) + { + int size = images[i].Width; // carre + writer.Write((byte)(size >= 256 ? 0 : size)); + writer.Write((byte)(size >= 256 ? 0 : size)); + writer.Write((byte)0); // palette + writer.Write((byte)0); // reserved + writer.Write((ushort)1); // planes + writer.Write((ushort)32); // bitcount + writer.Write((uint)pngBlocks[i].Length); + writer.Write((uint)offset); + offset += pngBlocks[i].Length; + } + + for (int i = 0; i < images.Count; i++) + { + writer.Write(pngBlocks[i]); + } + } + } + } +} diff --git a/src/MagnifierForm.cs b/src/MagnifierForm.cs new file mode 100644 index 0000000..5e1de7c --- /dev/null +++ b/src/MagnifierForm.cs @@ -0,0 +1,69 @@ +using System; +using System.Drawing; +using System.Windows.Forms; + +namespace ColorPickerApp +{ + /// Petite fenetre flottante qui suit le curseur et affiche la couleur survolee. + internal sealed class MagnifierForm : Form + { + private readonly Panel _swatch; + private readonly Label _label; + + public MagnifierForm() + { + FormBorderStyle = FormBorderStyle.None; + ShowInTaskbar = false; + StartPosition = FormStartPosition.Manual; + TopMost = true; + Size = new Size(150, 44); + BackColor = Color.FromArgb(32, 32, 32); + DoubleBuffered = true; + + _swatch = new Panel { Location = new Point(6, 6), Size = new Size(32, 32), BorderStyle = BorderStyle.FixedSingle }; + _label = new Label + { + Location = new Point(44, 6), + Size = new Size(100, 32), + ForeColor = Color.White, + Font = new Font("Consolas", 9F), + TextAlign = System.Drawing.ContentAlignment.MiddleLeft + }; + + Controls.Add(_swatch); + Controls.Add(_label); + } + + public void UpdateDisplay(Point screenPoint, Color color) + { + _swatch.BackColor = color; + _label.Text = string.Format("#{0:X2}{1:X2}{2:X2}", color.R, color.G, color.B); + + int x = screenPoint.X + 20; + int y = screenPoint.Y + 20; + + var screen = Screen.FromPoint(screenPoint).Bounds; + if (x + Width > screen.Right) x = screenPoint.X - Width - 10; + if (y + Height > screen.Bottom) y = screenPoint.Y - Height - 10; + + Location = new Point(x, y); + } + + protected override bool ShowWithoutActivation + { + get { return true; } + } + + protected override CreateParams CreateParams + { + get + { + const int WS_EX_NOACTIVATE = 0x08000000; + const int WS_EX_TOOLWINDOW = 0x00000080; + CreateParams cp = base.CreateParams; + cp.ExStyle |= WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW; + return cp; + } + } + } +} diff --git a/src/MainForm.cs b/src/MainForm.cs new file mode 100644 index 0000000..509af50 --- /dev/null +++ b/src/MainForm.cs @@ -0,0 +1,302 @@ +using System; +using System.Drawing; +using System.Windows.Forms; + +namespace ColorPickerApp +{ + internal sealed class MainForm : Form + { + private FlowLayoutPanel _paletteFlow; + private Panel _preview; + private TextBox _hexBox; + private TextBox _rgbBox; + private TextBox _folderPathBox; + private Label _statusLabel; + private Color _currentColor = Color.FromArgb(0, 120, 215); + + public MainForm() + { + BuildUi(); + PopulatePalette(); + SetCurrentColor(_currentColor); + } + + private void BuildUi() + { + Text = "ColorPicker - Palette & Couleur de dossier"; + StartPosition = FormStartPosition.CenterScreen; + ClientSize = new Size(760, 640); + MinimumSize = new Size(700, 600); + Font = new Font("Segoe UI", 9F); + + // ---- Palette (gauche) ---- + var paletteLabel = new Label + { + Text = "Palette de couleurs", + Location = new Point(16, 12), + AutoSize = true, + Font = new Font(Font, FontStyle.Bold) + }; + + _paletteFlow = new FlowLayoutPanel + { + Location = new Point(16, 36), + Size = new Size(400, 420), + AutoScroll = true, + BorderStyle = BorderStyle.FixedSingle, + BackColor = Color.White, + Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Bottom + }; + + // ---- Panneau droit : couleur courante ---- + var rightX = 432; + + var previewLabel = new Label { Text = "Couleur actuelle", Location = new Point(rightX, 12), AutoSize = true, Font = new Font(Font, FontStyle.Bold) }; + + _preview = new Panel + { + Location = new Point(rightX, 36), + Size = new Size(312, 80), + BorderStyle = BorderStyle.FixedSingle + }; + + var hexLabel = new Label { Text = "Code hexadécimal :", Location = new Point(rightX, 132), AutoSize = true }; + _hexBox = new TextBox { Location = new Point(rightX, 152), Size = new Size(150, 24), Font = new Font("Consolas", 10F), MaxLength = 7 }; + var hexCopyBtn = new Button { Text = "Copier", Location = new Point(rightX + 158, 151), Size = new Size(80, 26) }; + var hexApplyBtn = new Button { Text = "OK", Location = new Point(rightX + 242, 151), Size = new Size(70, 26) }; + + var rgbLabel = new Label { Text = "Code RGB :", Location = new Point(rightX, 190), AutoSize = true }; + _rgbBox = new TextBox { Location = new Point(rightX, 210), Size = new Size(150, 24), Font = new Font("Consolas", 10F), ReadOnly = true }; + var rgbCopyBtn = new Button { Text = "Copier", Location = new Point(rightX + 158, 209), Size = new Size(80, 26) }; + + var eyedropperBtn = new Button + { + Text = "Prélever une couleur à l'écran", + Location = new Point(rightX, 254), + Size = new Size(312, 32) + }; + + var customColorBtn = new Button + { + Text = "Couleur personnalisée...", + Location = new Point(rightX, 292), + Size = new Size(312, 32) + }; + + // ---- Groupe couleur de dossier ---- + var folderGroup = new GroupBox + { + Text = "Couleur d'un dossier de l'explorateur", + Location = new Point(16, 468), + Size = new Size(728, 140), + Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom + }; + + _folderPathBox = new TextBox + { + Location = new Point(12, 28), + Size = new Size(500, 24), + ReadOnly = true, + Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top + }; + var browseBtn = new Button { Text = "Parcourir...", Location = new Point(520, 27), Size = new Size(100, 26), Anchor = AnchorStyles.Right | AnchorStyles.Top }; + var refreshHintLabel = new Label + { + Text = "Astuce : si l'icône ne change pas immédiatement, fermez/rouvrez l'explorateur du dossier.", + Location = new Point(12, 58), + AutoSize = true, + ForeColor = Color.DimGray + }; + + var applyFolderBtn = new Button { Text = "Appliquer la couleur au dossier", Location = new Point(12, 88), Size = new Size(220, 32) }; + var restoreFolderBtn = new Button { Text = "Restaurer la couleur par défaut", Location = new Point(240, 88), Size = new Size(220, 32) }; + + _statusLabel = new Label { Location = new Point(472, 92), Size = new Size(240, 24), ForeColor = Color.DarkGreen, Anchor = AnchorStyles.Right | AnchorStyles.Bottom }; + + folderGroup.Controls.Add(_folderPathBox); + folderGroup.Controls.Add(browseBtn); + folderGroup.Controls.Add(refreshHintLabel); + folderGroup.Controls.Add(applyFolderBtn); + folderGroup.Controls.Add(restoreFolderBtn); + folderGroup.Controls.Add(_statusLabel); + + Controls.Add(paletteLabel); + Controls.Add(_paletteFlow); + Controls.Add(previewLabel); + Controls.Add(_preview); + Controls.Add(hexLabel); + Controls.Add(_hexBox); + Controls.Add(hexCopyBtn); + Controls.Add(hexApplyBtn); + Controls.Add(rgbLabel); + Controls.Add(_rgbBox); + Controls.Add(rgbCopyBtn); + Controls.Add(eyedropperBtn); + Controls.Add(customColorBtn); + Controls.Add(folderGroup); + + // ---- Evenements ---- + hexCopyBtn.Click += (s, e) => CopyToClipboard(_hexBox.Text); + rgbCopyBtn.Click += (s, e) => CopyToClipboard(_rgbBox.Text); + hexApplyBtn.Click += (s, e) => TryApplyHexInput(); + _hexBox.KeyDown += (s, e) => { if (e.KeyCode == Keys.Enter) { TryApplyHexInput(); e.SuppressKeyPress = true; } }; + + eyedropperBtn.Click += (s, e) => LaunchEyedropper(); + customColorBtn.Click += (s, e) => LaunchCustomColorDialog(); + + browseBtn.Click += (s, e) => BrowseFolder(); + applyFolderBtn.Click += (s, e) => ApplyFolderColor(); + restoreFolderBtn.Click += (s, e) => RestoreFolderColor(); + } + + private void PopulatePalette() + { + foreach (var color in Palette.Generate()) + { + var swatch = new Panel + { + Size = new Size(26, 26), + Margin = new Padding(2), + BackColor = color, + BorderStyle = BorderStyle.FixedSingle, + Cursor = Cursors.Hand, + Tag = color + }; + swatch.Click += (s, e) => SetCurrentColor((Color)((Panel)s).Tag); + _paletteFlow.Controls.Add(swatch); + } + } + + private void SetCurrentColor(Color color) + { + _currentColor = color; + _preview.BackColor = color; + + _hexBox.Text = string.Format("#{0:X2}{1:X2}{2:X2}", color.R, color.G, color.B); + _rgbBox.Text = string.Format("{0}, {1}, {2}", color.R, color.G, color.B); + } + + private void TryApplyHexInput() + { + string text = _hexBox.Text.Trim(); + if (!text.StartsWith("#")) text = "#" + text; + + try + { + if (text.Length != 7) throw new FormatException(); + int r = Convert.ToInt32(text.Substring(1, 2), 16); + int g = Convert.ToInt32(text.Substring(3, 2), 16); + int b = Convert.ToInt32(text.Substring(5, 2), 16); + SetCurrentColor(Color.FromArgb(r, g, b)); + } + catch + { + MessageBox.Show(this, "Code hexadécimal invalide. Format attendu : #RRGGBB", "Erreur", + MessageBoxButtons.OK, MessageBoxIcon.Warning); + SetCurrentColor(_currentColor); + } + } + + private void CopyToClipboard(string text) + { + if (string.IsNullOrEmpty(text)) return; + try + { + Clipboard.SetText(text); + ShowStatus("Copié : " + text, false); + } + catch (Exception ex) + { + ShowStatus("Erreur copie : " + ex.Message, true); + } + } + + private void LaunchEyedropper() + { + Hide(); + try + { + using (var picker = new ScreenColorPickerForm()) + { + picker.ShowDialog(); + if (picker.PickedColor != Color.Empty) + { + SetCurrentColor(picker.PickedColor); + } + } + } + finally + { + Show(); + Activate(); + } + } + + private void LaunchCustomColorDialog() + { + using (var dlg = new ColorDialog { FullOpen = true, Color = _currentColor, AnyColor = true }) + { + if (dlg.ShowDialog(this) == DialogResult.OK) + { + SetCurrentColor(dlg.Color); + } + } + } + + private void BrowseFolder() + { + using (var dlg = new FolderBrowserDialog { Description = "Choisir le dossier à colorer" }) + { + if (dlg.ShowDialog(this) == DialogResult.OK) + { + _folderPathBox.Text = dlg.SelectedPath; + ShowStatus("", false); + } + } + } + + private void ApplyFolderColor() + { + if (string.IsNullOrEmpty(_folderPathBox.Text)) + { + ShowStatus("Sélectionnez d'abord un dossier.", true); + return; + } + + try + { + FolderColorizer.ApplyColor(_folderPathBox.Text, _currentColor); + ShowStatus("Couleur appliquée au dossier.", false); + } + catch (Exception ex) + { + ShowStatus("Échec : " + ex.Message, true); + } + } + + private void RestoreFolderColor() + { + if (string.IsNullOrEmpty(_folderPathBox.Text)) + { + ShowStatus("Sélectionnez d'abord un dossier.", true); + return; + } + + try + { + FolderColorizer.RestoreDefault(_folderPathBox.Text); + ShowStatus("Icône par défaut restaurée.", false); + } + catch (Exception ex) + { + ShowStatus("Échec : " + ex.Message, true); + } + } + + private void ShowStatus(string message, bool isError) + { + _statusLabel.Text = message; + _statusLabel.ForeColor = isError ? Color.Firebrick : Color.DarkGreen; + } + } +} diff --git a/src/Palette.cs b/src/Palette.cs new file mode 100644 index 0000000..49d5c43 --- /dev/null +++ b/src/Palette.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Drawing; + +namespace ColorPickerApp +{ + /// Genere une palette complete (teintes x nuances + niveaux de gris). + internal static class Palette + { + public static List Generate() + { + var colors = new List(); + + const int hueSteps = 12; + const int shadeSteps = 8; + + for (int s = 0; s < shadeSteps; s++) + { + double lightness = 0.90 - (s * (0.80 / (shadeSteps - 1))); + for (int h = 0; h < hueSteps; h++) + { + double hue = (360.0 / hueSteps) * h; + colors.Add(FromHsl(hue, 0.75, lightness)); + } + } + + const int grayCount = 12; + for (int g = 0; g < grayCount; g++) + { + int v = (int)Math.Round(255.0 * g / (grayCount - 1)); + colors.Add(Color.FromArgb(v, v, v)); + } + + return colors; + } + + private static Color FromHsl(double h, double s, double l) + { + double c = (1 - Math.Abs(2 * l - 1)) * s; + double x = c * (1 - Math.Abs((h / 60.0) % 2 - 1)); + double m = l - c / 2; + + double r1, g1, b1; + if (h < 60) { r1 = c; g1 = x; b1 = 0; } + else if (h < 120) { r1 = x; g1 = c; b1 = 0; } + else if (h < 180) { r1 = 0; g1 = c; b1 = x; } + else if (h < 240) { r1 = 0; g1 = x; b1 = c; } + else if (h < 300) { r1 = x; g1 = 0; b1 = c; } + else { r1 = c; g1 = 0; b1 = x; } + + int r = (int)Math.Round((r1 + m) * 255); + int g = (int)Math.Round((g1 + m) * 255); + int b = (int)Math.Round((b1 + m) * 255); + + r = Math.Max(0, Math.Min(255, r)); + g = Math.Max(0, Math.Min(255, g)); + b = Math.Max(0, Math.Min(255, b)); + + return Color.FromArgb(r, g, b); + } + } +} diff --git a/src/Program.cs b/src/Program.cs new file mode 100644 index 0000000..fef894c --- /dev/null +++ b/src/Program.cs @@ -0,0 +1,16 @@ +using System; +using System.Windows.Forms; + +namespace ColorPickerApp +{ + internal static class Program + { + [STAThread] + private static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new MainForm()); + } + } +} diff --git a/src/ScreenColorPickerForm.cs b/src/ScreenColorPickerForm.cs new file mode 100644 index 0000000..568296e --- /dev/null +++ b/src/ScreenColorPickerForm.cs @@ -0,0 +1,94 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.Windows.Forms; + +namespace ColorPickerApp +{ + /// + /// Superpose une fenetre transparente plein ecran (tous moniteurs) permettant + /// de capturer la couleur de n'importe quel pixel affiche a l'ecran. + /// + internal sealed class ScreenColorPickerForm : Form + { + private readonly Timer _timer; + private readonly MagnifierForm _magnifier; + private Color _hoverColor = Color.White; + private Point _lastScreenPoint; + + public Color PickedColor { get; private set; } + + public ScreenColorPickerForm() + { + FormBorderStyle = FormBorderStyle.None; + ShowInTaskbar = false; + StartPosition = FormStartPosition.Manual; + TopMost = true; + BackColor = Color.Black; + Opacity = 0.01; // fenetre quasi invisible, sert uniquement a capter la souris + Cursor = Cursors.Cross; + DoubleBuffered = true; + KeyPreview = true; + + Bounds = SystemInformation.VirtualScreen; + + _magnifier = new MagnifierForm(); + + _timer = new Timer { Interval = 40 }; + _timer.Tick += (s, e) => UpdateHover(); + + MouseClick += OnMouseClick; + KeyDown += (s, e) => { if (e.KeyCode == Keys.Escape) { PickedColor = Color.Empty; Close(); } }; + } + + protected override void OnShown(EventArgs e) + { + base.OnShown(e); + _magnifier.Show(this); + _timer.Start(); + } + + protected override void OnFormClosed(FormClosedEventArgs e) + { + _timer.Stop(); + _magnifier.Close(); + base.OnFormClosed(e); + } + + private void UpdateHover() + { + _lastScreenPoint = Cursor.Position; + try + { + using (var bmp = new Bitmap(1, 1, PixelFormat.Format32bppArgb)) + { + using (var g = Graphics.FromImage(bmp)) + { + g.CopyFromScreen(_lastScreenPoint, Point.Empty, new Size(1, 1)); + } + _hoverColor = bmp.GetPixel(0, 0); + } + } + catch + { + // ignore capture errors (ex: DPI change) et garder la derniere couleur connue + } + + _magnifier.UpdateDisplay(_lastScreenPoint, _hoverColor); + } + + private void OnMouseClick(object sender, MouseEventArgs e) + { + if (e.Button == MouseButtons.Left) + { + PickedColor = _hoverColor; + Close(); + } + else + { + PickedColor = Color.Empty; + Close(); + } + } + } +} diff --git a/tests/IconPreview.cs b/tests/IconPreview.cs new file mode 100644 index 0000000..8da451b --- /dev/null +++ b/tests/IconPreview.cs @@ -0,0 +1,34 @@ +using System.Drawing; +using System.IO; + +namespace ColorPickerApp +{ + internal static class IconPreview + { + private static void Main(string[] args) + { + string dir = Path.Combine(Path.GetTempPath(), "cp_preview"); + Directory.CreateDirectory(dir); + FolderColorizer.ApplyColor(dir, Color.FromArgb(220, 40, 60)); + + byte[] icoBytes = File.ReadAllBytes(Path.Combine(dir, ".colorpicker_folder.ico")); + int count = System.BitConverter.ToUInt16(icoBytes, 4); + // trouver l'entree 256x256 (largeur stockee = 0) avec le plus grand bytesInRes + int bestOffset = -1, bestSize = -1, bestLen = 0; + for (int i = 0; i < count; i++) + { + int entryOffset = 6 + i * 16; + int w = icoBytes[entryOffset]; + int len = System.BitConverter.ToInt32(icoBytes, entryOffset + 8); + int off = System.BitConverter.ToInt32(icoBytes, entryOffset + 12); + if (w == 0 && len > bestLen) { bestLen = len; bestOffset = off; bestSize = w; } + } + var pngBytes = new byte[bestLen]; + System.Array.Copy(icoBytes, bestOffset, pngBytes, 0, bestLen); + File.WriteAllBytes(Path.Combine(Path.GetTempPath(), "cp_icon_preview.png"), pngBytes); + + FolderColorizer.RestoreDefault(dir); + Directory.Delete(dir, true); + } + } +} diff --git a/tests/TestHarness.cs b/tests/TestHarness.cs new file mode 100644 index 0000000..aea91c1 --- /dev/null +++ b/tests/TestHarness.cs @@ -0,0 +1,46 @@ +using System; +using System.Drawing; +using System.IO; + +namespace ColorPickerApp +{ + internal static class TestHarness + { + private static void Main(string[] args) + { + string testDir = Path.Combine(Path.GetTempPath(), "cp_test_" + Guid.NewGuid().ToString("N").Substring(0, 8)); + Directory.CreateDirectory(testDir); + Console.WriteLine("Dossier de test: " + testDir); + + FolderColorizer.ApplyColor(testDir, Color.FromArgb(220, 40, 60)); + Console.WriteLine("Apres ApplyColor:"); + foreach (var f in Directory.GetFiles(testDir, "*", SearchOption.TopDirectoryOnly)) + { + Console.WriteLine(" " + f + " attrs=" + File.GetAttributes(f)); + } + Console.WriteLine(" dossier attrs=" + new DirectoryInfo(testDir).Attributes); + Console.WriteLine(" HasCustomColor=" + FolderColorizer.HasCustomColor(testDir)); + + string iniPath = Path.Combine(testDir, "desktop.ini"); + Console.WriteLine("--- desktop.ini ---"); + Console.WriteLine(File.ReadAllText(iniPath)); + + string icoPath = Path.Combine(testDir, ".colorpicker_folder.ico"); + var icoBytes = File.ReadAllBytes(icoPath); + Console.WriteLine("Taille .ico: " + icoBytes.Length + " octets, count entries=" + BitConverter.ToUInt16(icoBytes, 4)); + + FolderColorizer.RestoreDefault(testDir); + Console.WriteLine("Apres RestoreDefault:"); + foreach (var f in Directory.GetFiles(testDir, "*", SearchOption.TopDirectoryOnly)) + { + Console.WriteLine(" " + f); + } + Console.WriteLine(" dossier attrs=" + new DirectoryInfo(testDir).Attributes); + Console.WriteLine(" HasCustomColor=" + FolderColorizer.HasCustomColor(testDir)); + Console.WriteLine("Fichiers restants: " + Directory.GetFiles(testDir).Length); + + Directory.Delete(testDir, true); + Console.WriteLine("OK - test termine sans exception."); + } + } +}