Hasta ahora, BiblioTech ha vivido enteramente en la consola: cada préstamo, cada alta de socio,
cada consulta al catálogo se ha mostrado con Console.WriteLine y se ha recibido con
Console.ReadLine. Esta lección da el primer paso hacia una interfaz real, con Windows
Forms (a menudo abreviado WinForms), el framework de interfaz gráfica más antiguo de .NET
que sigue en uso. Aunque hoy existen alternativas más modernas (WPF, Blazor, MAUI, que verás en
las próximas lecciones), Windows Forms sigue siendo una opción legítima y muy productiva para un
tipo concreto de aplicación: herramientas internas de escritorio en Windows, sin pretensiones
visuales sofisticadas, donde la velocidad de desarrollo importa más que la estética. Vas a
construir un formulario que muestra el catálogo de BiblioTech y permite prestar un libro con un
clic, reutilizando exactamente la misma lógica de dominio de los módulos anteriores.
Contenido
- Qué es Windows Forms y cuándo tiene sentido usarlo hoy
- Crear un proyecto con
dotnet new winforms - El diseñador de formularios y los controles básicos
- El modelo de eventos de los controles:
Clicky los delegados del Módulo 4 - Ejemplo completo: un formulario de préstamos para BiblioTech
- Buenas prácticas al conectar la interfaz con el dominio
- Qué es Windows Forms y cuándo tiene sentido usarlo hoy
Windows Forms apareció en el año 2002, junto con la primera versión de .NET, y fue durante años
la forma estándar de construir aplicaciones de escritorio en C#. Su modelo es sencillo: un
formulario (Form) es una ventana, sobre la que se colocan controles (botones, cajas de
texto, listas...) mediante un diseñador visual de arrastrar y soltar. Cada control expone
propiedades (posición, texto, color) y eventos (Click, TextChanged) a los que el programador
engancha código.
¿Sigue teniendo sentido en 2026? Sí, para un perfil de aplicación muy concreto:
| Característica | Windows Forms |
|---|---|
| Plataforma | Solo Windows (no es multiplataforma) |
| Curva de aprendizaje | Muy baja: el diseñador visual genera casi todo el código de la interfaz |
| Aspecto visual | Funcional, con el estilo nativo de Windows; no está pensado para diseños sofisticados |
| Separación UI/lógica | Débil por defecto (el código de eventos suele mezclarse con la lógica) |
| Caso de uso típico | Herramientas internas de empresa, utilidades administrativas, aplicaciones de gestión sencillas |
| Alternativa moderna recomendada para UI más cuidada | WPF (siguiente lección) |
Para BiblioTech, Windows Forms encaja perfectamente en un escenario: una aplicación interna que usa el personal de la biblioteca en los ordenadores de mostrador, sin necesidad de instalar nada más que .NET, y sin que el aspecto visual sea una prioridad frente a la rapidez de tener algo funcionando.
- Crear un proyecto con
dotnet new winforms
dotnet new winformsIgual que en módulos anteriores creaste proyectos de consola con dotnet new console, Windows
Forms tiene su propia plantilla:
Esto genera una estructura mínima:
Program.cs: el punto de entrada, que arranca la aplicación y muestra el primer formulario.Form1.cs: el código del formulario que tú escribes (eventos, lógica).Form1.Designer.cs: código generado automáticamente por el diseñador visual; no se edita a mano en circunstancias normales.Form1.resx: recursos del formulario (iconos, textos incrustados).
// Program.cs generado por la plantilla
ApplicationConfiguration.Initialize();
Application.Run(new Form1());Application.Run(new Form1()) crea una instancia del formulario principal y entra en el bucle
de mensajes de Windows: un bucle interno, gestionado por el framework, que espera eventos del
sistema operativo (clics de ratón, pulsaciones de teclado, repintados de pantalla) y los
despacha al formulario correspondiente. Este bucle es exactamente lo que hace que un clic en un
botón acabe ejecutando tu código: no es magia, es el mismo modelo de eventos que ya conoces del
Módulo 4, aplicado ahora a la interacción del usuario con el ratón y el teclado.
- El diseñador de formularios y los controles básicos
El diseñador de formularios (disponible en Visual Studio en Windows) permite arrastrar controles desde una caja de herramientas hasta el formulario, y ajustar sus propiedades desde un panel lateral, sin escribir código a mano. Los controles más habituales:
| Control | Para qué sirve | Propiedad principal |
|---|---|---|
Label |
Mostrar texto no editable (títulos, etiquetas) | Text |
TextBox |
Entrada de texto de una línea | Text |
Button |
Disparar una acción al hacer clic | Text (el texto del botón) |
ListBox |
Lista simple de elementos seleccionables | Items, SelectedItem |
DataGridView |
Tabla con filas y columnas, ideal para colecciones de objetos | DataSource |
Cuando arrastras un Button al formulario y cambias su propiedad Text a "Prestar", el
diseñador escribe automáticamente algo así en Form1.Designer.cs (código generado, no manual):
// Form1.Designer.cs (fragmento generado por el diseñador)
private Button botonPrestar;
private void InitializeComponent()
{
botonPrestar = new Button();
botonPrestar.Location = new Point(120, 200);
botonPrestar.Size = new Size(100, 30);
botonPrestar.Text = "Prestar";
botonPrestar.Click += botonPrestar_Click; // conexion del evento, ver apartado 4
// ...
Controls.Add(botonPrestar);
}Controls.Add(botonPrestar) añade el botón a la colección Controls del formulario —el
conjunto de controles hijos que se dibujan sobre él—; sin esa línea, el botón existiría como
objeto en memoria pero nunca aparecería en pantalla.
- El modelo de eventos de los controles:
Click y los delegados del Módulo 4
Click y los delegados del Módulo 4La lección de Delegados y Eventos (Módulo 4) explicó que un evento es, en esencia, una lista de
métodos (delegados) que se ejecutan cuando algo ocurre —allí el ejemplo fue
Biblioteca.PrestamoRegistrado—. Los controles de Windows Forms usan exactamente el mismo
mecanismo: Button.Click es un evento de tipo EventHandler, al que te suscribes con +=,
igual que hiciste con PrestamoRegistrado:
public partial class FormularioPrincipal : Form
{
public FormularioPrincipal()
{
InitializeComponent(); // generado por el diseñador: crea y coloca los controles
botonPrestar.Click += BotonPrestar_Click; // suscripcion al evento, igual que en el Modulo 4
}
private void BotonPrestar_Click(object? remitente, EventArgs argumentos)
{
// codigo que se ejecuta cada vez que el usuario hace clic en "Prestar"
}
}La firma (object? remitente, EventArgs argumentos) es el patrón estándar de eventos de .NET:
remitente es el objeto que disparó el evento (aquí, botonPrestar) y argumentos transporta
información adicional (vacía en Click, pero no en otros eventos como KeyPress, que incluye la
tecla pulsada). Reconocer esta firma es clave: es la misma estructura object, EventArgs que
usan prácticamente todos los eventos de la biblioteca estándar de .NET, no solo los de Windows
Forms.
- Ejemplo completo: un formulario de préstamos para BiblioTech
Se reutiliza el dominio ya construido —Biblioteca, MaterialBibliotecario, Libro,
Socio— sin modificar ni una sola clase. El formulario solo añade una capa visual encima:
// FormularioPrincipal.cs
public partial class FormularioPrincipal : Form
{
private readonly Biblioteca _biblioteca;
public FormularioPrincipal(Biblioteca biblioteca)
{
InitializeComponent();
_biblioteca = biblioteca;
CargarCatalogoEnListBox();
botonPrestar.Click += BotonPrestar_Click;
}
private void CargarCatalogoEnListBox()
{
listaCatalogo.Items.Clear();
foreach (MaterialBibliotecario material in _biblioteca.Catalogo)
{
// ToString() no esta sobrescrito en MaterialBibliotecario, asi que se compone el texto aqui
listaCatalogo.Items.Add($"{material.Titulo} ({material.Autor})");
}
}
private void BotonPrestar_Click(object? remitente, EventArgs argumentos)
{
int indiceSeleccionado = listaCatalogo.SelectedIndex;
if (indiceSeleccionado < 0)
{
MessageBox.Show("Selecciona un libro de la lista antes de prestar.", "Aviso");
return;
}
MaterialBibliotecario materialSeleccionado = _biblioteca.Catalogo[indiceSeleccionado];
if (!materialSeleccionado.Disponible)
{
MessageBox.Show($"'{materialSeleccionado.Titulo}' ya esta prestado.", "Aviso");
return;
}
materialSeleccionado.Prestar(); // logica de dominio ya existente, Modulo 2
MessageBox.Show($"Prestamo registrado: {materialSeleccionado.Titulo}", "Prestamo realizado");
CargarCatalogoEnListBox(); // refresca la lista para reflejar el nuevo estado
}
}// Program.cs
Biblioteca biblioteca = new Biblioteca();
biblioteca.AgregarMaterial(new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4"));
biblioteca.AgregarMaterial(new Libro("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1"));
ApplicationConfiguration.Initialize();
Application.Run(new FormularioPrincipal(biblioteca));Ningún concepto de Biblioteca, MaterialBibliotecario o Libro ha cambiado: el formulario se
limita a leer _biblioteca.Catalogo para pintar la ListBox, y a llamar a
materialSeleccionado.Prestar() cuando el usuario hace clic —exactamente el mismo método que ya
usabas desde la consola en el Módulo 2. MessageBox.Show(...) muestra un cuadro de diálogo
emergente, la forma más simple de comunicar un mensaje al usuario en Windows Forms, equivalente
visual del Console.WriteLine de los módulos anteriores.
Para un catálogo más grande, un DataGridView con listaCatalogo.DataSource = _biblioteca.Catalogo
(en vez de una ListBox rellenada manualmente) mostraría automáticamente una tabla con una
columna por propiedad pública, aunque requiere ajustar qué columnas mostrar para no exponer
Disponible como texto plano sin formato.
- Buenas prácticas al conectar la interfaz con el dominio
- El formulario no debe contener lógica de negocio:
BotonPrestar_Clickllama amaterialSeleccionado.Prestar(), no reimplementa la comprobación de disponibilidad dentro del formulario —esa lógica ya vive, correctamente, enMaterialBibliotecariodesde el Módulo 2. - Pasar
Bibliotecaal constructor del formulario, como se hace aquí, en vez de crearla dentro del propio formulario: mantiene el formulario desacoplado de cómo se construye o configuraBiblioteca(con persistencia en texto, JSON, SQLite... según lo visto en el Módulo 5), un principio que se retomará con más profundidad en el Módulo 8 al hablar de inyección de dependencias. - Refrescar la interfaz tras cada cambio de estado: como en
CargarCatalogoEnListBox()después dePrestar(), para que lo que ve el usuario coincida siempre con el estado real de_biblioteca.
Errores Comunes y Consejos
- Editar
Form1.Designer.csa mano: es código generado por el diseñador visual; cualquier cambio hecho a mano puede perderse la próxima vez que se edite el formulario desde el diseñador. Los cambios de comportamiento van en el fichero.cs"normal" (Form1.cs), no en el.Designer.cs. - Olvidar comprobar
SelectedIndex < 0: si el usuario hace clic en "Prestar" sin haber seleccionado nada en laListBox,SelectedIndexvale-1; acceder al catálogo con ese índice lanzaría una excepción en tiempo de ejecución. - Mezclar lógica de negocio dentro del método de evento: duplicar aquí la comprobación de
Disponibleen vez de delegarla enPrestar()(que ya la hace, Módulo 2) generaría dos fuentes de verdad que podrían desincronizarse con el tiempo. - Consejo: Windows Forms solo compila y ejecuta en Windows; si tu equipo de desarrollo usa distintos sistemas operativos, ten esto en cuenta antes de elegirlo para un proyecto nuevo.
Ejercicios
-
Añade a
FormularioPrincipalunTextBoxllamadocuadroTituloy un segundoButtonllamadobotonBuscar. En el eventoClickdebotonBuscar, filtra_biblioteca.Catalogopor título (usandoContains, ya visto con cadenas en el Módulo 1) y muestra solo los resultados enlistaCatalogo. -
Modifica
BotonPrestar_Clickpara que, en vez deMessageBox.Show, muestre el mensaje de confirmación en unLabelllamadoetiquetaEstadosituado en la parte inferior del formulario.
Soluciones
private void BotonBuscar_Click(object? remitente, EventArgs argumentos)
{
string textoBusqueda = cuadroTitulo.Text;
listaCatalogo.Items.Clear();
IEnumerable<MaterialBibliotecario> coincidencias = _biblioteca.Catalogo
.Where(material => material.Titulo.Contains(textoBusqueda, StringComparison.OrdinalIgnoreCase));
foreach (MaterialBibliotecario material in coincidencias)
{
listaCatalogo.Items.Add($"{material.Titulo} ({material.Autor})");
}
}
private void BotonPrestar_Click(object? remitente, EventArgs argumentos)
{
int indiceSeleccionado = listaCatalogo.SelectedIndex;
if (indiceSeleccionado < 0)
{
etiquetaEstado.Text = "Selecciona un libro de la lista antes de prestar.";
return;
}
MaterialBibliotecario materialSeleccionado = _biblioteca.Catalogo[indiceSeleccionado];
if (!materialSeleccionado.Disponible)
{
etiquetaEstado.Text = $"'{materialSeleccionado.Titulo}' ya esta prestado.";
return;
}
materialSeleccionado.Prestar();
etiquetaEstado.Text = $"Prestamo registrado: {materialSeleccionado.Titulo}";
CargarCatalogoEnListBox();
}
Conclusión
En esta lección has construido la primera interfaz gráfica real de BiblioTech con Windows Forms:
un proyecto dotnet new winforms, controles básicos (ListBox, Button, Label) colocados con
el diseñador visual, y el modelo de eventos de controles (Click), que resulta ser exactamente
el mismo mecanismo de delegados y eventos del Módulo 4 aplicado a la interacción del usuario. Todo
ello sin tocar una sola línea de Biblioteca, MaterialBibliotecario o Libro: la interfaz es
una capa nueva encima del dominio ya construido.
La siguiente lección presenta WPF, una alternativa más moderna a Windows Forms, con una diferencia de fondo importante: en vez de colocar controles imperativamente desde el diseñador, WPF describe la interfaz de forma declarativa con XAML, y separa con más disciplina la interfaz de la lógica mediante el patrón MVVM y el data binding — verás el mismo catálogo de BiblioTech, pero conectado a la interfaz de una forma bastante distinta.
Curso de Programación en C#
Módulo 1: Introducción a C#
- Introducción a C#
- Configuración del Entorno de Desarrollo
- Programa Hola Mundo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Arrays y Cadenas de Texto
Módulo 2: Estructuras de Control
Módulo 3: Programación Orientada a Objetos
- Clases y Objetos
- Métodos
- Constructores y Destructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- Structs y Records: Tipos por Valor y por Referencia
Módulo 4: Conceptos Avanzados de C#
- Interfaces
- Delegados y Eventos
- Pattern Matching y Características Modernas de C#
- Genéricos
- Colecciones
- LINQ (Consulta Integrada en el Lenguaje)
- Programación Asíncrona
Módulo 5: Trabajando con Datos
- Entrada/Salida de Archivos
- Serialización
- Conectividad con Bases de Datos
- Entity Framework
- Trabajo con JSON y Consumo de APIs REST
Módulo 6: Temas Avanzados
- Reflexión
- Atributos
- Programación Dinámica
- Gestión de Memoria y Recolección de Basura
- Multihilo y Programación Paralela
Módulo 7: Construcción de Aplicaciones
- Formularios de Windows
- WPF (Windows Presentation Foundation)
- ASP.NET Core
- Blazor
- Xamarin y .NET MAUI
Módulo 8: Mejores Prácticas y Patrones de Diseño
- Estándares de Codificación y Mejores Prácticas
- Patrones de Diseño
- Inyección de Dependencias e Inversión de Control
- Pruebas Unitarias
- Revisión y Refactorización de Código
