Skip to content
← Tous les articles
· Delta1 Labs WinFormsGlass

Décompiler une app WinForms : reconstruire l'InitializeComponent du concepteur et les ressources

Une app WinForms compilée n'a ni .Designer.cs ni .resx : le code du concepteur a été fusionné dans un seul type sous la forme d'InitializeComponent(), et les textes et images de l'interface vivent dans un flux .resources incorporé. Voici comment Glass.NET reconstruit un InitializeComponent() fidèle à partir du binaire, fait apparaître les valeurs du ComponentResourceManager et vous laisse exporter les ressources incorporées : tout en statique, tout gratuitement.

Ouvrez un projet WinForms dans Visual Studio et chaque formulaire est fait de deux fichiers. Il y a MainForm.cs, où vit votre code — le constructeur, les gestionnaires de clic de bouton, la logique que vous avez réellement écrite — et il y a MainForm.Designer.cs, qui appartient au concepteur. Ce second fichier contient une seule méthode générée, InitializeComponent(), plus les champs privés de chaque contrôle du formulaire. Vous l’ouvrez rarement ; le concepteur le réécrit chaque fois que vous glissez quelque chose sur la surface.

Puis vous distribuez l’app, vous perdez le code source, et des années plus tard vous en avez besoin. Vous ouvrez l’.exe dans un décompilateur et les deux fichiers ont disparu. Il n’y a pas de MainForm.Designer.cs, pas de .resx, juste un type MainForm avec tout dedans. Ce n’est pas un décompilateur en échec : c’est ainsi que WinForms a toujours été compilé. Cet article parcourt ce que le compilateur a fait à votre code de concepteur, et comment Glass.NET — le décompilateur .NET gratuit et purement statique — le reconstruit en quelque chose que vous pouvez lire et à partir duquel vous pouvez reconstruire.

La séparation en deux fichiers est une illusion propre au code source

La séparation entre MainForm.cs et MainForm.Designer.cs est une classe partielle C#. Les deux fichiers déclarent partial class MainForm, et le compilateur les fusionne en un seul type avant même d’émettre le moindre IL. La séparation existe uniquement pour que le concepteur ait un fichier qu’il puisse régénérer sans toucher à votre code. Le CLR n’en a aucune notion.

Supposons que vous ayez construit un tout petit formulaire avec un libellé et un bouton. Votre moitié ressemble à ceci :

// MainForm.cs — la partie que vous écrivez
public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();
    }

    private void greetButton_Click(object sender, EventArgs e)
    {
        greetingLabel.Text = $"Hello at {DateTime.Now:T}";
    }
}

La moitié du concepteur — le fichier que vous n’ouvrez presque jamais — ressemble à ceci :

// MainForm.Designer.cs — la partie qui appartient au concepteur
partial class MainForm
{
    private System.Windows.Forms.Label greetingLabel;
    private System.Windows.Forms.Button greetButton;

    private void InitializeComponent()
    {
        this.greetingLabel = new System.Windows.Forms.Label();
        this.greetButton = new System.Windows.Forms.Button();
        this.SuspendLayout();
        //
        // greetingLabel
        //
        this.greetingLabel.AutoSize = true;
        this.greetingLabel.Location = new System.Drawing.Point(24, 24);
        this.greetingLabel.Name = "greetingLabel";
        this.greetingLabel.Size = new System.Drawing.Size(120, 15);
        this.greetingLabel.TabIndex = 0;
        this.greetingLabel.Text = "Not greeted yet";
        //
        // greetButton
        //
        this.greetButton.Anchor = System.Windows.Forms.AnchorStyles.Bottom;
        this.greetButton.Location = new System.Drawing.Point(24, 64);
        this.greetButton.Name = "greetButton";
        this.greetButton.Size = new System.Drawing.Size(120, 32);
        this.greetButton.TabIndex = 1;
        this.greetButton.Text = "Greet me";
        this.greetButton.Click += new System.EventHandler(this.greetButton_Click);
        //
        // MainForm
        //
        this.ClientSize = new System.Drawing.Size(300, 160);
        this.Controls.Add(this.greetButton);
        this.Controls.Add(this.greetingLabel);
        this.Name = "MainForm";
        this.Text = "Greeter";
        this.ResumeLayout(false);
        this.PerformLayout();
    }
}

Deux fichiers au clavier, un seul type dans l’assembly. Le compilateur prend les déclarations de champs du fichier du concepteur, les méthodes des deux fichiers, et émet un seul type MainForm. InitializeComponent() devient une méthode d’instance privée ordinaire, indiscernable dans les métadonnées de n’importe quelle méthode que vous auriez écrite à la main.

Ce que le binaire contient réellement

DANS LE CODE (deux fichiers)MainForm.csvotre logique,gestionnairesMainForm.Designer.csInitializeComponent()champs de contrôlesDANS LE BINAIRE (un type)type MainFormprivate Label greetingLabelprivate Button greetButtonInitializeComponent()greetButton_Click().ctor()MainForm.resourceschaînes · images · icônes(compilé depuis .resx)GLASS RECONSTRUITMainForm (un .cs)InitializeComponent() fidèleSuspendLayout / ResumeLayoutLocation · Size · Text · AnchorControls.Add(...)Click += greetButton_Clickvaleurs de ressources récupéréestexte lisible, images exportées

Lorsque Glass ouvre l’assembly, le type MainForm apparaît avec ses membres présentés clairement : les champs greetingLabel et greetButton, le constructeur, votre gestionnaire greetButton_Click et InitializeComponent(). Les champs privés des contrôles qui vivaient dans le fichier du concepteur sont bien là en tant que membres du type — private Label greetingLabel; et private Button greetButton; — parce que c’est exactement là que le compilateur les a placés.

Reconstruire InitializeComponent()

InitializeComponent() n’est qu’une méthode, donc Glass la décompile comme il décompile n’importe quelle méthode : en lisant l’IL et en le relevant vers du C#. Le concepteur émet un IL très régulier, presque mécanique — construire un contrôle, affecter propriété après propriété, l’ajouter à la collection Controls du parent — qui est exactement le genre de code linéaire qu’un décompilateur reconstruit proprement. Ce que Glass produit se lit pour ainsi dire comme le fichier du concepteur que vous avez perdu :

private void InitializeComponent()
{
    this.greetingLabel = new Label();
    this.greetButton = new Button();
    this.SuspendLayout();
    this.greetingLabel.AutoSize = true;
    this.greetingLabel.Location = new Point(24, 24);
    this.greetingLabel.Name = "greetingLabel";
    this.greetingLabel.Size = new Size(120, 15);
    this.greetingLabel.TabIndex = 0;
    this.greetingLabel.Text = "Not greeted yet";
    this.greetButton.Anchor = AnchorStyles.Bottom;
    this.greetButton.Location = new Point(24, 64);
    this.greetButton.Name = "greetButton";
    this.greetButton.Size = new Size(120, 32);
    this.greetButton.TabIndex = 1;
    this.greetButton.Text = "Greet me";
    this.greetButton.Click += this.greetButton_Click;
    this.ClientSize = new Size(300, 160);
    this.Controls.Add(this.greetButton);
    this.Controls.Add(this.greetingLabel);
    this.Name = "MainForm";
    this.Text = "Greeter";
    this.ResumeLayout(false);
    this.PerformLayout();
}

Tout ce qui comptait est de retour. L’encadrement SuspendLayout()/ResumeLayout() qui empêche le formulaire de s’agiter pendant l’ajout des contrôles ; les affectations de Location, Size, Text et Anchor ; les appels à Controls.Add(...) dans leur ordre d’origine (qui est l’ordre-z, indépendant de TabIndex) ; et le câblage d’événements — this.greetButton.Click += this.greetButton_Click; — pointant vers la méthode gestionnaire qui se trouve dans le même type. Le corps du gestionnaire lui-même, greetButton_Click, est décompilé comme une méthode normale ; il n’a jamais rien eu de spécial. Déposez ceci dans un fichier avec vos gestionnaires reconstruits et vous avez un formulaire qui compile.

La seule différence cosmétique, ce sont les bandeaux de commentaires // que le concepteur écrit au-dessus de chaque bloc de contrôle. Ce sont des commentaires du code source ; ils ne sont pas compilés en IL, donc ils ont disparu. Le code est équivalent, simplement sans commentaires.

Les ressources du concepteur et le ComponentResourceManager

Le formulaire ci-dessus affectait son texte avec des littéraux de chaîne incorporés directement dans l’IL (this.greetButton.Text = "Greet me";). Les formulaires localisables fonctionnent différemment. Lorsque vous mettez la propriété Localizable d’un formulaire à true, le concepteur cesse d’écrire des littéraux et achemine à la place chaque propriété à travers un ComponentResourceManager :

private void InitializeComponent()
{
    ComponentResourceManager resources =
        new ComponentResourceManager(typeof(MainForm));
    this.greetButton = new Button();
    this.SuspendLayout();
    resources.ApplyResources(this.greetButton, "greetButton");
    this.greetButton.Name = "greetButton";
    this.greetButton.Click += this.greetButton_Click;
    resources.ApplyResources(this, "$this");
    this.Controls.Add(this.greetButton);
    this.ResumeLayout(false);
}

Maintenant greetButton.Text, sa Location, sa Size et même une image ou une icône ne sont plus du tout dans le code. ApplyResources(this.greetButton, "greetButton") recherche chaque entrée préfixée par greetButton. — greetButton.Text, greetButton.Size, greetButton.Location — dans une table de ressources et les applique. Cette table est le flux .resources incorporé, nommé <Namespace>.<Form>.resources : pour un formulaire Greeter.MainForm dans l’espace de noms Greeter, le flux est Greeter.MainForm.resources. Il a été compilé à partir du MainForm.resx que vous éditiez au moment de la conception, et les variantes par culture (MainForm.fr.resx, MainForm.de.resx) ont été compilées dans des assemblys satellites sous les dossiers fr/ et de/.

Glass décompile fidèlement les appels à ApplyResources : vous pouvez voir quels contrôles sont pilotés par les ressources et sous quelle clé. Mais le point intéressant, c’est qu’il fait aussi apparaître le flux de ressources incorporé lui-même. Déployez le nœud des ressources de l’assembly et sélectionnez Greeter.MainForm.resources, et Glass liste les entrées que ApplyResources extrait à l’exécution : greetButton.Text = "Greet me", greetButton.Size = un Size sérialisé, $this.Text = "Greeter", et toute image ou icône sous forme de valeur binaire que vous pouvez visualiser et exporter. C’est ainsi que vous lisez les vraies chaînes d’un formulaire localisé — y compris les françaises et les allemandes dans les assemblys satellites — sans l’exécuter, et que vous récupérez les images et icônes que ApplyResources câblait.

L’exécuter depuis la CLI

Tout ce qui précède est automatisable. La sous-commande glass decompile reconstruit le type sans interface, ce que vous voulez sur un dossier de binaires récupérés :

$ glass decompile ./Greeter.exe --type Greeter.MainForm -o ./recovered/MainForm.cs

Cela écrit le MainForm reconstruit — champs, InitializeComponent(), gestionnaires et tout — dans un seul fichier. Pour extraire les ressources du concepteur à côté, Glass exporte le flux .resources incorporé et son contenu, de sorte que les chaînes, images et icônes atterrissent sur le disque où vous pouvez les inspecter ou les reconvertir en un .resx. Chacune de ces commandes lit le binaire et reconstruit à partir des métadonnées et de l’IL ; aucune n’exécute l’assembly, donc un .exe WinForms que vous ne lanceriez jamais reste sûr à décompiler et à exporter.

Ce que vous récupérez, et ce que vous ne récupérez pas

Soyez lucide sur les limites, car elles découlent directement de ce que le compilateur conserve :

  • Vous récupérez un InitializeComponent() fidèle — la création des contrôles, SuspendLayout/ResumeLayout, chaque affectation de propriété, l’ordre de Controls.Add et le câblage d’événements — plus les champs des contrôles, les corps des gestionnaires et les valeurs des ressources du flux incorporé.
  • Vous ne récupérez pas la séparation d’origine en deux fichiers. Il n’y a jamais eu de MainForm.Designer.cs dans le binaire ; il y avait un seul type MainForm. Glass vous donne un seul fichier. Si vous voulez que le concepteur rouvre le formulaire, re-scindez-le à la main en un partiel .Designer.cs : c’est un geste mécanique.
  • Vous ne récupérez pas le XML .resx original. Ce format de création est compilé et disparaît dans le flux binaire .resources et n’est jamais distribué. Mais comme les valeurs sont toutes récupérables depuis le flux, vous pouvez régénérer un .resx à partir d’elles si vous avez de nouveau besoin du format de création.

En résumé : l’intention du concepteur survit entièrement à la compilation — la disposition, le câblage, les chaînes et les images sont tous là — même si les deux fichiers et le XML .resx qui l’exprimaient, eux, ne survivent pas. Glass lit ce que le binaire a conservé et vous le rend sous forme de C# lisible et de données de ressources récupérables.

C’est exactement le flux pour récupérer votre propre code source WinForms perdu, ou pour comprendre une dépendance dont vous devez raisonner sur l’interface. Téléchargez Glass.NET depuis la page du produit et pointez-le sur un formulaire. C’est gratuit — sans licence, sans sièges, sans inscription — et il n’exécute jamais l’app qu’il lit.

Essayez Nebula.NET

Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.