Descompilar una app WinForms: reconstruir el InitializeComponent del diseñador y los recursos
Una app WinForms compilada no tiene .Designer.cs ni .resx: el código del diseñador se fusionó en un único tipo como InitializeComponent(), y los textos e imágenes de la interfaz viven en un stream .resources incrustado. Así reconstruye Glass.NET un InitializeComponent() fiel a partir del binario, muestra los valores del ComponentResourceManager y te deja exportar los recursos incrustados: todo estático, todo gratis.
Abre un proyecto WinForms en Visual Studio y cada formulario son dos archivos. Está MainForm.cs, donde vive tu código —el constructor, los manejadores de clic de botón, la lógica que de verdad escribiste— y está MainForm.Designer.cs, que pertenece al diseñador. Ese segundo archivo contiene un único método generado, InitializeComponent(), además de los campos privados de cada control del formulario. Rara vez lo abres; el diseñador lo reescribe cada vez que arrastras algo a la superficie.
Después distribuyes la app, pierdes el código fuente y, años más tarde, lo necesitas de vuelta. Abres el .exe en un descompilador y los dos archivos ya no están. No hay MainForm.Designer.cs, ni .resx, solo un tipo MainForm con todo dentro. Eso no es un descompilador fallando: es como WinForms siempre se compiló. Este artículo recorre lo que el compilador le hizo a tu código de diseñador y cómo Glass.NET —el descompilador .NET gratuito y puramente estático— lo reconstruye en algo que puedes leer y a partir de lo cual reconstruir.
La división en dos archivos es una ilusión que solo existe en el código fuente
La división entre MainForm.cs y MainForm.Designer.cs es una clase parcial de C#. Ambos archivos declaran partial class MainForm, y el compilador los fusiona en un único tipo antes de emitir ningún IL. La división existe únicamente para que el diseñador tenga un archivo que pueda regenerar sin tocar tu código. El CLR no tiene ningún concepto de ella.
Supón que construiste un formulario diminuto con una etiqueta y un botón. Tu mitad se ve así:
// MainForm.cs — la parte que escribes tú
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
private void greetButton_Click(object sender, EventArgs e)
{
greetingLabel.Text = $"Hello at {DateTime.Now:T}";
}
}
La mitad del diseñador —el archivo que casi nunca abres— se ve así:
// MainForm.Designer.cs — la parte que pertenece al diseñador
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();
}
}
Dos archivos en el teclado, un tipo en el ensamblado. El compilador toma las declaraciones de campos del archivo del diseñador, los métodos de ambos archivos, y emite un único tipo MainForm. InitializeComponent() se convierte en un método de instancia privado corriente, indistinguible en los metadatos de cualquier método que escribieras a mano.
Lo que el binario contiene en realidad
Cuando Glass abre el ensamblado, el tipo MainForm aparece con sus miembros dispuestos con claridad: los campos greetingLabel y greetButton, el constructor, tu manejador greetButton_Click e InitializeComponent(). Los campos privados de los controles que vivían en el archivo del diseñador están justo ahí como miembros del tipo —private Label greetingLabel; y private Button greetButton;— porque es exactamente donde el compilador los puso.
Reconstruir InitializeComponent()
InitializeComponent() no es más que un método, así que Glass lo descompila como descompila cualquier método: leyendo el IL y elevándolo de vuelta a C#. El diseñador emite un IL muy regular, casi mecánico —construir un control, asignar propiedad tras propiedad, añadirlo a la colección Controls del padre— que es justo el tipo de código lineal que un descompilador reconstruye de forma limpia. Lo que Glass produce se lee prácticamente como el archivo del diseñador que perdiste:
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();
}
Todo lo que importaba ha vuelto. El par SuspendLayout()/ResumeLayout() que impide que el formulario se sacuda mientras se añaden los controles; las asignaciones de Location, Size, Text y Anchor; las llamadas a Controls.Add(...) en su orden original (que es el orden-z, independiente de TabIndex); y el cableado de eventos —this.greetButton.Click += this.greetButton_Click;— apuntando al método manejador que está en el mismo tipo. El cuerpo del propio manejador, greetButton_Click, se descompila como un método normal; nunca tuvo nada de especial. Pon esto en un archivo con tus manejadores reconstruidos y tendrás un formulario que compila.
La única diferencia cosmética son los banners de comentarios // que el diseñador escribe encima de cada bloque de control. Esos son comentarios del código fuente; no se compilan en IL, así que se pierden. El código es equivalente, solo que sin comentarios.
Los recursos del diseñador y el ComponentResourceManager
El formulario de arriba asignaba su texto con literales de cadena incrustados directamente en el IL (this.greetButton.Text = "Greet me";). Los formularios localizables funcionan de otra manera. Cuando pones la propiedad Localizable de un formulario en true, el diseñador deja de escribir literales y en su lugar enruta cada propiedad a través de 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);
}
Ahora greetButton.Text, su Location, su Size e incluso una imagen o un icono no están en el código en absoluto. ApplyResources(this.greetButton, "greetButton") busca cada entrada con el prefijo greetButton. —greetButton.Text, greetButton.Size, greetButton.Location— en una tabla de recursos y las aplica. Esa tabla es el stream .resources incrustado, llamado <Namespace>.<Form>.resources: para un formulario Greeter.MainForm en el espacio de nombres Greeter, el stream es Greeter.MainForm.resources. Se compiló a partir del MainForm.resx que editaste en tiempo de diseño, y las variantes por cultura (MainForm.fr.resx, MainForm.de.resx) se compilaron en ensamblados satélite bajo las carpetas fr/ y de/.
Glass descompila las llamadas a ApplyResources con fidelidad: puedes ver qué controles están gobernados por recursos y bajo qué clave. Pero lo interesante es que también muestra el propio stream de recursos incrustado. Expande el nodo de recursos del ensamblado y selecciona Greeter.MainForm.resources, y Glass lista las entradas que ApplyResources extrae en tiempo de ejecución: greetButton.Text = "Greet me", greetButton.Size = un Size serializado, $this.Text = "Greeter", y cualquier imagen o icono como un valor binario que puedes ver y exportar. Así es como lees las cadenas reales de un formulario localizado —incluidas las francesas y alemanas en los ensamblados satélite— sin ejecutarlo, y como recuperas las imágenes e iconos que ApplyResources cableaba.
Ejecutarlo desde la CLI
Todo lo anterior es automatizable. El subcomando glass decompile reconstruye el tipo de forma headless, que es lo que quieres sobre una carpeta de binarios recuperados:
$ glass decompile ./Greeter.exe --type Greeter.MainForm -o ./recovered/MainForm.cs
Eso escribe el MainForm reconstruido —campos, InitializeComponent(), manejadores y todo— en un único archivo. Para extraer los recursos del diseñador junto a él, Glass exporta el stream .resources incrustado y su contenido, de modo que las cadenas, imágenes e iconos aterrizan en disco donde puedes inspeccionarlos o volver a convertirlos en un .resx. Cada uno de estos lee el binario y reconstruye a partir de metadatos e IL; ninguno ejecuta el ensamblado, así que un .exe de WinForms que nunca lanzarías sigue siendo seguro de descompilar y exportar.
Lo que recuperas y lo que no
Sé claro sobre los límites, porque se derivan directamente de lo que el compilador conserva:
- Recuperas un
InitializeComponent()fiel —la creación de controles,SuspendLayout/ResumeLayout, cada asignación de propiedad, el orden deControls.Addy el cableado de eventos— además de los campos de los controles, los cuerpos de los manejadores y los valores de los recursos del stream incrustado. - No recuperas la división original en dos archivos. Nunca hubo un
MainForm.Designer.csen el binario; había un único tipoMainForm. Glass te da un solo archivo. Si quieres que el diseñador reabra el formulario, vuelve a dividirlo a mano en un parcial.Designer.cs: es un movimiento mecánico. - No recuperas el XML
.resxoriginal. Ese formato de autoría se compila y desaparece en el stream binario.resourcesy nunca se distribuye. Pero como los valores son todos recuperables del stream, puedes regenerar un.resxa partir de ellos si vuelves a necesitar el formato de autoría.
En resumen: la intención del diseñador sobrevive a la compilación por completo —la disposición, el cableado, las cadenas y las imágenes están todos ahí— aunque los dos archivos y el XML .resx que la expresaban no. Glass lee lo que el binario conservó y te lo devuelve como C# legible y datos de recursos recuperables.
Este es exactamente el flujo para recuperar tu propio código fuente WinForms perdido, o para entender una dependencia cuya interfaz necesitas razonar. Descarga Glass.NET desde la página del producto y apúntalo a un formulario. Es gratis —sin licencia, sin asientos, sin registro— y nunca ejecuta la app que lee.
Prueba Nebula.NET
Endurece tu código .NET en minutos — empieza con la edición gratuita.