From c3330204e3b1ed97db2443aa8f416861c7ecd80c Mon Sep 17 00:00:00 2001 From: Julien Cluzel Date: Mon, 8 Jun 2026 19:30:32 +0200 Subject: [PATCH] Initial : marketplace + 2 plugins (syncfusion-ribbon-accent, offscreen-winforms-render) --- .claude-plugin/marketplace.json | 28 +++ .gitignore | 3 + LICENSE | 21 ++ README.md | 43 ++++ .../.claude-plugin/plugin.json | 11 + .../skills/offscreen-winforms-render/SKILL.md | 89 ++++++++ .../.claude-plugin/plugin.json | 11 + .../skills/syncfusion-ribbon-accent/SKILL.md | 195 ++++++++++++++++++ 8 files changed, 401 insertions(+) create mode 100644 .claude-plugin/marketplace.json create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 plugins/offscreen-winforms-render/.claude-plugin/plugin.json create mode 100644 plugins/offscreen-winforms-render/skills/offscreen-winforms-render/SKILL.md create mode 100644 plugins/syncfusion-ribbon-accent/.claude-plugin/plugin.json create mode 100644 plugins/syncfusion-ribbon-accent/skills/syncfusion-ribbon-accent/SKILL.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..62c1d76 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://code.claude.com/docs/plugins/marketplace-schema.json", + "name": "ara-claude-plugins", + "description": "Plugins Claude Code pour le développement WinForms / Syncfusion (ARA Informatique).", + "owner": { + "name": "Julien Cluzel" + }, + "plugins": [ + { + "name": "syncfusion-ribbon-accent", + "source": "./plugins/syncfusion-ribbon-accent", + "description": "Couleur d'accent personnalisée (live) sur un ruban Syncfusion WinForms Office2016 : ruban, backstage (dont le SURVOL des items), onglets de contenu, grilles SfDataGrid. Le bon levier par élément + les pièges.", + "version": "1.0.0", + "license": "MIT", + "keywords": ["syncfusion", "winforms", "ribbon", "office2016", "theme", "accent", "backstage", "sfdatagrid"], + "category": "dev" + }, + { + "name": "offscreen-winforms-render", + "source": "./plugins/offscreen-winforms-render", + "description": "Vérifier/déboguer l'aspect d'une IHM WinForms sans bureau interactif (agent/CI/headless) : rendu hors écran via PrintWindow → PNG relisible et échantillonnable au pixel.", + "version": "1.0.0", + "license": "MIT", + "keywords": ["winforms", "render", "offscreen", "printwindow", "headless", "screenshot", "testing"], + "category": "dev" + } + ] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3433d43 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +bin/ +*.log diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1cccd16 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Julien Cluzel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7972fa7 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# ara-claude-plugins + +Marketplace de **plugins Claude Code** pour le développement **WinForms / Syncfusion**. +Chaque plugin apporte un *skill* réutilisable, issu d'un projet réel (migration WinDev → .NET + Syncfusion). + +## Plugins + +| Plugin | Skill | Pour quoi | +|---|---|---| +| **syncfusion-ribbon-accent** | `/syncfusion-ribbon-accent:syncfusion-ribbon-accent` | Appliquer une **couleur d'accent personnalisée** (live) à un ruban Syncfusion WinForms Office2016 : ruban, backstage (dont le **survol** des items), onglets de contenu, grilles `SfDataGrid`. Le bon levier par élément + les pièges. | +| **offscreen-winforms-render** | `/offscreen-winforms-render:offscreen-winforms-render` | **Vérifier/déboguer une IHM WinForms sans bureau interactif** (agent, CI, headless) : rendu hors écran via `PrintWindow` → PNG relisible et échantillonnable au pixel. | + +Réf. testée : Syncfusion Essential Studio **WinForms 33.2.x**, .NET, C#. + +## Installation + +```text +/plugin marketplace add https://repos.ara-it.info/ARA-IT/ara-claude-plugins.git +/plugin install syncfusion-ribbon-accent@ara-claude-plugins +/plugin install offscreen-winforms-render@ara-claude-plugins +``` + +Puis `/reload-plugins` si installé en cours de session. Les skills s'invoquent ensuite via +`/:` (ou se déclenchent automatiquement selon le contexte). + +Mettre à jour : `/plugin marketplace update ara-claude-plugins`. + +## Structure du repo + +``` +.claude-plugin/marketplace.json # catalogue (liste les 2 plugins) +plugins/ +├── syncfusion-ribbon-accent/ +│ ├── .claude-plugin/plugin.json +│ └── skills/syncfusion-ribbon-accent/SKILL.md +└── offscreen-winforms-render/ + ├── .claude-plugin/plugin.json + └── skills/offscreen-winforms-render/SKILL.md +``` + +## Licence + +MIT — voir [LICENSE](LICENSE). diff --git a/plugins/offscreen-winforms-render/.claude-plugin/plugin.json b/plugins/offscreen-winforms-render/.claude-plugin/plugin.json new file mode 100644 index 0000000..cd07591 --- /dev/null +++ b/plugins/offscreen-winforms-render/.claude-plugin/plugin.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://code.claude.com/docs/plugins/plugin-schema.json", + "name": "offscreen-winforms-render", + "description": "Vérifier ou déboguer l'aspect visuel d'une IHM WinForms (.NET, y compris Syncfusion) sans bureau interactif (agent, CI, headless) — rendu hors écran via PrintWindow + thread STA → PNG relisible ET échantillonnable au pixel. Inclut : forcer un état de survol, ouvrir une backstage, injecter une couleur de test.", + "version": "1.0.0", + "author": { + "name": "Julien Cluzel" + }, + "license": "MIT", + "repository": "https://repos.ara-it.info/ARA-IT/ara-claude-plugins" +} diff --git a/plugins/offscreen-winforms-render/skills/offscreen-winforms-render/SKILL.md b/plugins/offscreen-winforms-render/skills/offscreen-winforms-render/SKILL.md new file mode 100644 index 0000000..b886c96 --- /dev/null +++ b/plugins/offscreen-winforms-render/skills/offscreen-winforms-render/SKILL.md @@ -0,0 +1,89 @@ +--- +name: offscreen-winforms-render +description: À utiliser pour VÉRIFIER ou DÉBOGUER l'aspect visuel d'une IHM WinForms (.NET, y compris Syncfusion) quand il n'y a PAS de bureau interactif (agent, CI, headless) — où CopyFromScreen échoue. Rend la fenêtre HORS ÉCRAN dans un PNG (PrintWindow + thread STA) que l'on peut relire ET échantillonner au pixel (preuve empirique des couleurs/du layout). Inclut : forcer un état de survol, ouvrir une backstage, injecter une couleur de test. +--- + +# Rendu d'artefact offscreen (vérifier une IHM WinForms sans la lancer) + +**But** : produire un **PNG** du rendu réel d'une fenêtre WinForms sans bureau interactif, pour le **relire** +et l'**échantillonner pixel par pixel** (preuve chiffrée). Portable : tout projet .NET WinForms (Syncfusion ou non). + +## Pourquoi + +- En agent/CI, pas de bureau interactif : `Graphics.CopyFromScreen` échoue (« Descripteur non valide »). +- Sans retour visuel, on tourne en rond sur les bugs d'IHM (couleur, layout). +- Parade : **dessiner la fenêtre hors écran dans un bitmap** via `PrintWindow`, sauver un PNG, puis (1) le relire, + (2) l'échantillonner (`bmp.GetPixel(x,y)`) pour comparer des valeurs RGB attendues. + +## Prérequis + +- Console jetable `net10.0-windows` (`OutputType=WinExe`, `UseWindowsForms=true`). Dossier préfixé `_` (gitignoré). +- Si l'IHM a une **licence** (Syncfusion…) : l'enregistrer, sinon un dialogue modal **gèle** le rendu. +- Si l'IHM a un **thème** chargé au démarrage : le charger pareil que l'app. +- Pour rendre la VRAIE fenêtre : référencer le projet applicatif (`ProjectReference`). + +## Gabarit + +```csharp +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +var th = new Thread(Run); // 1) thread STA obligatoire +th.SetApartmentState(ApartmentState.STA); +th.Start(); th.Join(); + +[DllImport("user32.dll")] static extern bool PrintWindow(IntPtr h, IntPtr hdc, uint flags); + +void Run() +{ + // 2) Licence éventuelle (sinon dialogue d'essai modal => blocage) + // Syncfusion.Licensing.SyncfusionLicenseProvider.RegisterLicense("..."); + // 3) Thème éventuel, chargé comme dans l'app réelle + // SfSkinManager.LoadAssembly(typeof(Office2016Theme).Assembly); + + var f = new MyForm { StartPosition = FormStartPosition.Manual, Location = new Point(-3000, 0) }; // HORS écran + f.Show(); + for (int i = 0; i < 10; i++) { Application.DoEvents(); Thread.Sleep(80); } // 4) laisser peindre (PAS Application.Run) + + using var bmp = new Bitmap(f.Width, f.Height); + using (var g = Graphics.FromImage(bmp)) + { + var hdc = g.GetHdc(); + PrintWindow(f.Handle, hdc, 2); // 5) flag 2 = PW_RENDERFULLCONTENT (obligatoire) + g.ReleaseHdc(hdc); + } + bmp.Save(@"C:\chemin\rendu.png"); + var c = bmp.GetPixel(450, 12); // 6) preuve empirique chiffrée + Console.WriteLine($"pixel = {c.R},{c.G},{c.B}"); + f.Close(); +} +``` + +## Recettes utiles + +- **Plusieurs variantes en une exécution** : une fonction `Capture(name, build)` par variante, échantillonner le pixel décisif → comparer les RGB. Injecter une **couleur de test distinctive** (rouge/orange/violet), pas la couleur par défaut, pour distinguer « appliqué » de « inchangé ». +- **Rendre le VRAI form** (un mock minimal ne reproduit pas certains bugs de thème/skin) : référencer le projet, instancier la fenêtre. Accéder aux champs privés par réflexion si besoin : + ```csharp + var fld = typeof(MyForm).GetField("backStageView", BindingFlags.NonPublic | BindingFlags.Instance); + ``` +- **Ouvrir une backstage Syncfusion** : `backStageView.ShowBackStage();` +- **Forcer un état de survol** (capturer un hover sans souris) : `button.State = ButtonAdvState.MouseOver;` puis `Invalidate()`. (⚠️ un `State` forcé n'est pas toujours fidèle à 100 % au survol réel selon le contrôle — recouper avec la valeur de propriété et/ou un test live.) + +## Pièges (vérifiés) + +- **Thread STA obligatoire** (`SetApartmentState(STA)`), sinon plantage à l'instanciation. +- **Licence avant tout** : sans enregistrement, le dialogue d'essai modal gèle le rendu. +- **Thème chargé** comme dans l'app, sinon rendu non représentatif. +- **`PrintWindow` flag = 2** (`PW_RENDERFULLCONTENT`) : `0`/`1` ratent les contrôles peints en différé (noir/partiel). +- **Fenêtre hors écran** (`Location` négatif + `StartPosition=Manual`). +- **Laisser peindre** : `Show()` + boucle `Application.DoEvents()`/`Sleep` (PAS `Application.Run`). +- **Coordonnées d'échantillonnage** : repérer les bons `(x,y)` en regardant d'abord le PNG (un contrôle peut être repositionné au runtime ≠ ses `Bounds` de design). +- **Pas de `CopyFromScreen`** en headless (échoue) — c'est ce que `PrintWindow` contourne. + +## Workflow + +1. Créer `_render/` (gitignoré). +2. Composer un contrôle minimal OU instancier la vraie fenêtre ; faire plusieurs variantes ; échantillonner. +3. **Relire** le PNG (juger l'aspect) **et** lire la sortie console (preuve chiffrée). +4. Itérer, puis **supprimer** le dossier jetable. diff --git a/plugins/syncfusion-ribbon-accent/.claude-plugin/plugin.json b/plugins/syncfusion-ribbon-accent/.claude-plugin/plugin.json new file mode 100644 index 0000000..410e87b --- /dev/null +++ b/plugins/syncfusion-ribbon-accent/.claude-plugin/plugin.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://code.claude.com/docs/plugins/plugin-schema.json", + "name": "syncfusion-ribbon-accent", + "description": "Couleur d'accent personnalisée (live) sur un ruban Syncfusion WinForms Office2016 — ruban, backstage (dont le survol des items), onglets de contenu (TabControlAdv), grilles SfDataGrid. Donne le bon levier par élément et les pièges (ThemeStyle ignoré, ThemeName à conserver, BackStageItemHoverColor, FlattenHierarchy…). Réf. testée : Syncfusion WinForms 33.2.x.", + "version": "1.0.0", + "author": { + "name": "Julien Cluzel" + }, + "license": "MIT", + "repository": "https://repos.ara-it.info/ARA-IT/ara-claude-plugins" +} diff --git a/plugins/syncfusion-ribbon-accent/skills/syncfusion-ribbon-accent/SKILL.md b/plugins/syncfusion-ribbon-accent/skills/syncfusion-ribbon-accent/SKILL.md new file mode 100644 index 0000000..470daa5 --- /dev/null +++ b/plugins/syncfusion-ribbon-accent/skills/syncfusion-ribbon-accent/SKILL.md @@ -0,0 +1,195 @@ +--- +name: syncfusion-ribbon-accent +description: À utiliser pour IMPLÉMENTER ou DÉBOGUER une couleur d'accent personnalisée (live, choisie par l'utilisateur) sur une IHM Syncfusion WinForms Office2016 — ruban (RibbonControlAdv), backstage (BackStage/menu Fichier, y compris le SURVOL des items), onglets de contenu (TabControlAdv) et grilles (SfDataGrid). Donne le bon levier par élément et les pièges (ThemeStyle ignoré, ThemeName à ne pas vider, BackStageItemHoverColor, FlattenHierarchy…). Réf. testée : Syncfusion WinForms 33.2.x. +--- + +# Couleur d'accent sur un ruban Syncfusion WinForms (Office2016) + +**But** : faire suivre une **couleur d'accent libre** à TOUTE l'IHM (ruban, backstage, onglets, grilles), en live. +**Testé** : Syncfusion Essential Studio **WinForms 33.2.10**, .NET 10, C#. +**Prérequis** : `SfSkinManager.LoadAssembly(typeof(Office2016Theme).Assembly);` au démarrage. + +## Règle d'or — chaque zone a SON mécanisme (ne pas les confondre) + +| Élément | Bon levier (33.2.x) | À NE PAS faire | +|---|---|---| +| **Ruban** (bandeau, onglets, Fichier, boutons système) | `ribbon.Office2016ColorTable` (**Clear + Add d'UNE table**), en **gardant** `ThemeName="Office2016"` | `ribbon.ThemeStyle` (ignoré en live) ; vider `ThemeName` (ruban plat « touch ») | +| **Backstage — titre + bouton retour ←** | `ribbon.ThemeStyle.BackStageStyle` (`TitleBarBackColor`, `ControlBoxBackColor`…), **ré-assigner** `ribbon.ThemeStyle` | — | +| **Backstage — items (onglets+boutons) repos/SURVOL/sélection** | `ribbon.Office2016ColorTable.BackStageItem*` (le renderer `BackStage2016` lit cette table) | `BackStageButton.BackColor`/`MetroColor`/`OverrideFormManagedColor`/`ThemeStyle` (écrasés par le renderer) | +| **Backstage — onglet actif** | `backStage.ActiveTabColor` / `ActiveTabForeColor` | — | +| **Onglets de contenu** (`TabControlAdv`) | `content.ActiveTabColor` / `ActiveTabForeColor` | — | +| **Grilles** (`SfDataGrid`) | surcharge directe de `grid.Style` | chercher un `ThemeSettings` (inexistant en 33.2.10) | +| **QuickAccess Toolbar** (retrait) | `ribbon.QuickPanelVisible=false` + `ribbon.ShowQuickItemsDropDownButton=false` | — | + +**3 principes** : (1) **garder** `ribbon.ThemeName="Office2016"` (le vider casse le rendu) ; (2) `Office2016ColorTable` est une `BindingList` **sans clé** → **une seule table** (`Clear()`+`Add()`) ; (3) le renderer Office2016 lit cette table pour le ruban **ET** la backstage. + +## Pièges (à ne pas redécouvrir) + +1. `ribbon.ThemeStyle` n'a **aucun effet live** sur le bandeau du ruban (le skin repeint par-dessus) → `Office2016ColorTable`. +2. Vider `ribbon.ThemeName` colore le bandeau MAIS retire le skin → rendu plat « touch », laid. **Garder `"Office2016"`.** +3. `Office2016ColorTable` = `BindingList` sans clé/scheme → **une seule entrée** suffit. +4. **Survol des items backstage** : ni `BackStageButton.BackColor`/`MetroColor`/`OverrideFormManagedColor`+`CustomManagedColor`/`Appearance=Metro`/`ButtonAdvVisualStyle.HoverBackColor` — le renderer peint et lit la **color table** → **`BackStageItemHoverColor`**. +5. `backStage.ThemeName` se fait **réécraser** par SfSkinManager (`BackStage2016Renderer`) ; le vider est inopérant. +6. **Noms `[Obsolete]`** : `BackStage*Tab*Color`/`BackStage*Button*Color` → unifiés en **`BackStageItem*`**. +7. `SfSkinManager` 33.2.10 = `LoadAssembly` + `SetTheme` seulement (pas de `ThemeSettings`/`RegisterThemeSettings`). +8. `Office2016ColorTable` ne s'applique que sous `Office2016ColorScheme.Colorful`. +9. **Réflexion incomplète = fausse conclusion** : lister les propriétés d'un type Syncfusion SANS `BindingFlags.FlattenHierarchy` rate les **membres hérités** (piège qui avait fait conclure à tort que `BackStageItemHoverColor` n'existait pas). **Toujours `Public|Instance|FlattenHierarchy`** + signaler les `[Obsolete]`. + +## Vérification (environnement sans bureau / agent) + +- **Rendre le VRAI form** offscreen (un mock minimal ne reproduit PAS le bleu backstage). `PrintWindow` (flag 2) + thread STA → PNG relisible + `bmp.GetPixel`. Cf. méthode de rendu offscreen. +- Ouvrir la backstage par réflexion : `backStageView.ShowBackStage()` (champ privé). +- **Forcer un survol** pour le capturer sans souris : `btnX.State = ButtonAdvState.MouseOver;` puis `Invalidate()`. +- Injecter une **couleur de test distinctive** (orange/rouge/violet), pas l'accent par défaut, pour distinguer « appliqué » de « inchangé ». +- **Vérifier l'API par réflexion AVANT de coder** (`MetadataLoadContext`, `FlattenHierarchy`). + +## Checklist d'intégration + +1. `SfSkinManager.LoadAssembly(typeof(Office2016Theme).Assembly)`. +2. Ruban : `RibbonStyle.Office2016` + `Office2016ColorScheme.Colorful` + `ThemeName="Office2016"` + `CanOverrideStyle=true` + `MenuColor=accent`. +3. **Une** `Office2016ColorTable` (chrome=accent, corps clair, `BackStageItem*`=accent) → `Clear()`+`Add()`. +4. `ThemeStyle.BackStageStyle` : `TitleBarBackColor`/`ControlBoxBackColor` (+hover/pressed) → ré-assigner `ThemeStyle`. +5. `backStage.ActiveTabColor` + `content.ActiveTabColor`. +6. Grilles : `grid.Style.*` (sélection/en-tête/bordure/ascenseurs) + ré-application live. +7. QAT retirée si inutile. +8. Helpers `ForeColorFor` / `Shade` / `Tint`. +9. **Vérifier au rendu offscreen** (vrai form + survol forcé) avant de conclure. + +## Implémentation de référence (copiable) + +```csharp +using Syncfusion.Windows.Forms; // BackStage, BackStageButton +using Syncfusion.Windows.Forms.Tools; // RibbonForm, RibbonControlAdv, Office2016ColorTable, TabControlAdv +using Syncfusion.WinForms.Controls; // SfSkinManager +using Syncfusion.WinForms.DataGrid; // SfDataGrid + +internal static class AppTheme +{ + public static Color AccentColor { get; set; } = ColorTranslator.FromHtml("#0099CC"); + public const string SfThemeName = "Office2016Colorful"; + + // === RUBAN + BACKSTAGE (titre/bouton retour) === + public static void ApplyOffice2016(RibbonForm form, RibbonControlAdv ribbon, Color accent) + { + ribbon.RibbonStyle = RibbonStyle.Office2016; + ribbon.Office2016ColorScheme = Office2016ColorScheme.Colorful; + ribbon.ThemeName = "Office2016"; + ribbon.CanOverrideStyle = true; + ribbon.MenuColor = accent; + + ribbon.Office2016ColorTable.Clear(); + ribbon.Office2016ColorTable.Add(BuildOffice2016ColorTable(accent)); + + Color fore = ForeColorFor(accent); + var themeStyle = ribbon.ThemeStyle; + var bs = themeStyle.BackStageStyle; + bs.TitleBarBackColor = accent; bs.TitleBarForeColor = fore; + bs.ControlBoxBackColor = accent; // bouton retour ← + bs.HoverControlBoxBackColor = Shade(accent, 0.12); + bs.PressedControlBoxBackColor = Shade(accent, 0.20); + bs.ItemBackColor = accent; bs.ItemForeColor = fore; + bs.SelectedItemBackColor = Shade(accent, 0.20); bs.SelectedItemForeColor = fore; + bs.HoverItemBackColor = Shade(accent, 0.12); bs.HoverItemForeColor = fore; + ribbon.ThemeStyle = themeStyle; + + ribbon.Refresh(); + ribbon.Invalidate(true); + } + + private static Office2016ColorTable BuildOffice2016ColorTable(Color accent) + { + Color fore = ForeColorFor(accent); + Color body = ColorTranslator.FromHtml("#F1F1F1"); + Color bodyText = ColorTranslator.FromHtml("#575757"); + Color border = ColorTranslator.FromHtml("#D2D2D2"); + Color hover = ColorTranslator.FromHtml("#C5C5C5"); + return new Office2016ColorTable + { + // Chrome à l'accent + HeaderColor = accent, TabBackColor = accent, TitleColor = fore, + TabForeColor = fore, HoverTabForeColor = fore, SelectedTabColor = accent, + CheckedTabColor = body, CheckedTabForeColor = accent, + FileMenuForeColor = fore, FileMenuHoverForeColor = fore, + SystemButtonBackground = accent, ControlBoxForeColor = fore, + CloseButtonBackground = ColorTranslator.FromHtml("#E81123"), + // Items backstage (le renderer lit cette table) — SURVOL = BackStageItemHoverColor + BackStageItemForeColor = fore, + BackStageItemHoverColor = Shade(accent, 0.16), + BackStageItemHoverForeColor = fore, + BackStageItemSelectionColor = Shade(accent, 0.26), + BackStageItemSelectionForeColor = fore, + BackStageTitleColor = ColorTranslator.FromHtml("#3A3A3A"), + // Corps clair, lisible + CaptionBackColor = body, CaptionForeColor = bodyText, + ToolStripBackColor = body, PanelBackColor = body, RibbonPanelBorderColor = border, + ToolStripItemForeColor = bodyText, ToolStripSpliterColor = border, + ButtonBackgroundHovered = hover, ButtonBackgroundSelected = hover, + ButtonBackgroundPressed = ColorTranslator.FromHtml("#969696"), + }; + } + + // === ONGLETS DE CONTENU === + public static void ApplyContentTabs(TabControlAdv content, Color accent) + { + content.ActiveTabColor = accent; + content.ActiveTabForeColor = ForeColorFor(accent); + content.Refresh(); content.Invalidate(true); + } + + // === BACKSTAGE : onglet actif (le reste via la color table) === + public static void ApplyBackStageItems(BackStage backStage, Color accent) + { + backStage.ActiveTabColor = accent; + backStage.ActiveTabForeColor = ForeColorFor(accent); + backStage.Invalidate(true); + } + + // === GRILLES === + public static void ApplySfControlTheme(Control control) => SfSkinManager.SetTheme(control, SfThemeName); + + public static void ApplyGridAccent(SfDataGrid grid, Color accent) + { + grid.Style.SelectionStyle.BackColor = Tint(accent, 0.55); + grid.Style.SelectionStyle.TextColor = Color.Black; + grid.Style.HeaderStyle.SortIconColor = accent; + grid.Style.HeaderStyle.FilterIconColor = accent; + grid.Style.BorderColor = accent; + grid.Style.VerticalScrollBar.ThumbColor = Tint(accent, 0.45); + grid.Style.VerticalScrollBar.ThumbHoverColor = accent; + grid.Style.HorizontalScrollBar.ThumbColor = Tint(accent, 0.45); + grid.Style.HorizontalScrollBar.ThumbHoverColor = accent; + grid.Invalidate(); + } + + public static void RefreshGridAccents(Control root, Color accent) + { + foreach (Control c in root.Controls) + { + if (c is SfDataGrid g) ApplyGridAccent(g, accent); + if (c.HasChildren) RefreshGridAccents(c, accent); + } + } + + // === HELPERS === + private static Color ForeColorFor(Color c) + => (0.299 * c.R + 0.587 * c.G + 0.114 * c.B) / 255.0 > 0.55 ? Color.Black : Color.White; + public static Color Tint(Color c, double t) => Color.FromArgb(c.A, + (int)(c.R + (255 - c.R) * t), (int)(c.G + (255 - c.G) * t), (int)(c.B + (255 - c.B) * t)); + private static Color Shade(Color c, double f) => Color.FromArgb(c.A, + (int)(c.R * (1 - f)), (int)(c.G * (1 - f)), (int)(c.B * (1 - f))); +} +``` + +### Câblage (au démarrage ET dans le picker de couleur) + +```csharp +AppTheme.ApplyOffice2016(this, ribbon, accent); +AppTheme.ApplyContentTabs(content, accent); +AppTheme.ApplyBackStageItems(backStage, accent); +AppTheme.RefreshGridAccents(content, accent); // grilles déjà ouvertes +// QAT : ribbon.QuickPanelVisible = false; ribbon.ShowQuickItemsDropDownButton = false; +// + persister la couleur et la relire au démarrage +``` + +> Guide long + récit complet : `docs/syncfusion-ribbon-couleur-accent.md` (dans le repo d'origine CapToilettage). +> Technique de vérification : `docs/methode-rendu-offscreen.md`.