Controles con posición y tamaño adaptativo (ejercicio)
IronPython - Diseño WinForms: Ejercicio
Adaptar la posición y el tamaño de los controles al redimensionar el formulario
En la sección anterior vimos como hacer que un botón permanezca siempre centrado aunque cambie el tamaño del formulario.
Ahora te propongo un ejercicio mas complejo. El objetivo es crear un formulario con 6 botones, y que entre todos ocupen todo el espacio disponible aunque cambie el tamaño del formulario. Hay que conseguir lo siguiente:
Es decir, los botones se alinean formando 3 filas y 2 columnas.
¿Cómo lo harías?
Empecemos heredando un poco de código de la sección anterior, con algunas modificaciones. Definiremos una clase MainForm
así:
class MainForm(Form):
def __init__(self):
Form.__init__(self)
self.Text = 'Mi App'
self.Icon = Icon.ExtractAssociatedIcon(sys.executable)
self.ClientSize = Size(300, 300)
self.add_controls()
self.resize_controls()
self.Resize += self.resize_controls
def add_controls(self):
pass
def resize_controls(self, *args):
pass
Tenemos todo listo para añadir controles en el método add_controls
y para ajustar su posición y tamaño cuando cambien las dimensiones del formulario en el método resize_controls
.
¿Cómo lo harías?
Añadir los 6 botones
Podrías añadir 6 botones de manera secuencial de la siguiente manera:
def add_controls(self):
self.btn1 = Button()
self.btn2 = Button()
self.btn3 = Button()
self.btn4 = Button()
self.btn5 = Button()
self.btn6 = Button()
self.btn1.Text = 'btn1'
self.btn2.Text = 'btn2'
self.btn3.Text = 'btn3'
self.btn4.Text = 'btn4'
self.btn5.Text = 'btn5'
self.btn6.Text = 'btn6'
self.Controls.Add(self.btn1)
self.Controls.Add(self.btn2)
self.Controls.Add(self.btn3)
self.Controls.Add(self.btn4)
self.Controls.Add(self.btn5)
self.Controls.Add(self.btn6)
Pero te propongo una manera de escribir mucho menos código:
def add_controls(self):
self.buttons = [btn() for btn in [Button] * 6]
for btn in self.buttons:
btn.Text = 'btn' + str(self.buttons.index(btn) + 1)
self.Controls.Add(btn)
Todos los botones están incluidos en la lista self.buttons
. para no escribir Button()
6 veces he utilizado una lista de compresión. A continuación iteramos sobre self.buttons
para asignar a cada uno de ellos un Text
basado en su posición en la lista, y añadimos cada botón a la colección de controles del formulario: self.Controls.Add(btn)
.
Adaptar su posición y tamaño
En el método __init__
se invoca al método resize_controls
y se registra como manejador del evento Resize
, de manera que tanto al cargar el formulario como al redimensionarlo se ejecutará el método resize_controls
.
Dentro de este método tenemos que calcular la posición y el tamaño de los 6 botones en función del valor actual de la propiedad ClientSize
del formulario.
Como queremos que los botones se dispongan formando 2 columnas, cada botón debe tener un ancho de self.ClientSize.Width/2
. Y como queremos que formen 3 filas, cada botón debe tener una altura de self.ClientSize.Height/3
:
def resize_controls(self, *args):
width, height = self.ClientSize.Width/2, self.ClientSize.Height/3
if sys.implementation.name == 'cpython':
width, height = int(width), int(height)
for btn in self.buttons:
btn.Size = Size(width, height)
Pero todavía nos falta asignar las posiciones de los botones. Para ello voy a crear un diccionario de duplas:
positions = dict(
btn1=(0, 0), btn2=(width, 0),
btn3=(0, height), btn4=(width, height),
btn5=(0, height*2), btn6=(width, height*2)
)
Las claves del diccionario positions
corresponden a la propiedad Text
de cada uno de los botones en self.buttons
(podría haberse hecho con el índice de cada botón en la lista, pero para no utilizar tantos números he optado por utilizar los Text
).
El valor asignado a cada una de las claves del diccionario positions
en una tupla de 2 elementos: El primer elemento representa la posición en X y el segundo la posición en Y. Observa que la posición de cada botón está basada en el valor de las variables width
y height
que hemos calculado anteriormente.
Al iterar sobre self.buttons
asignaremos su nueva posición a cada uno de los botones, basada en el diccionario positions
:
def resize_controls(self, *args):
width, height = self.ClientSize.Width/2, self.ClientSize.Height/3
positions = dict(
btn1=(0, 0), btn2=(width, 0),
btn3=(0, height), btn4=(width, height),
btn5=(0, height*2), btn6=(width, height*2)
)
if sys.implementation.name == 'cpython':
width, height = int(width), int(height)
positions = {i:(int(j[0]), int(j[1])) for i,j in positions.items()}
for btn in self.buttons:
btn.Size = Size(width, height)
btn.Location = Point(*positions[btn.Text])
El resultado es el siguiente:
Comprueba como se ajusta la posición y tamaño de los botones redimensionando el formulario.
Observa que el primer botón está seleccionado (tiene el foco). Si quieres evitar esto puedes crear una clase que herede de Button
:
class CustomButton(Button):
def __init__(self):
Button.__init__(self)
self.SetStyle(ControlStyles.Selectable, False)
Y en el método add_controls
sustituir Button
por CustomButton
.
Código final
Antes de mostrar el código completo, voy a establecer un tamaño mínimo para el formulario. En el método __init__
voy a registrar un manejador para el evento Load
en el que asignaré el valor del Size
inicial del formulario a su MinumumSize
(hay que hacerlo después de que suceda el evento Load
porque antes no hay un Size
definido):
self.Load += lambda *args: self.__setattr__('MinimumSize', self.Size)
Recuerda que ClientSize
es el tamaño del área en el que se dibujan los controles, mientras que Size
es el tamaño del formulario incluyendo la caja de control.
Código final del Ejercicio:
# -*- coding: utf-8 -*-
import sys, clr
clr.AddReference('System.Windows.Forms')
clr.AddReference('System.Drawing')
from System.Windows.Forms import *
from System.Drawing import *
class CustomButton(Button):
def __init__(self):
Button.__init__(self)
self.SetStyle(ControlStyles.Selectable, False)
class MainForm(Form):
def __init__(self):
Form.__init__(self)
self.Text = 'Mi App'
self.Icon = Icon.ExtractAssociatedIcon(sys.executable)
self.ClientSize = Size(300, 300)
self.add_controls()
self.resize_controls()
self.Resize += self.resize_controls
self.Load += lambda *args: self.__setattr__('MinimumSize', self.Size)
def add_controls(self):
self.buttons = [btn() for btn in [CustomButton] * 6]
for btn in self.buttons:
btn.Text = 'btn' + str(self.buttons.index(btn) + 1)
self.Controls.Add(btn)
def resize_controls(self, *args):
width, height = self.ClientSize.Width/2, self.ClientSize.Height/3
positions = dict(
btn1=(0, 0), btn2=(width, 0),
btn3=(0, height), btn4=(width, height),
btn5=(0, height*2), btn6=(width, height*2)
)
if sys.implementation.name == 'cpython':
width, height = int(width), int(height)
positions = {i:(int(j[0]), int(j[1])) for i,j in positions.items()}
for btn in self.buttons:
btn.Size = Size(width, height)
btn.Location = Point(*positions[btn.Text])
if __name__ == '__main__':
Application.EnableVisualStyles()
Application.Run(MainForm())
En la siguiente sección aprenderás a utilizar las propiedades Anchor y Dock de los controles.
Un saludo!