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:

Formulario con 6 botones (3 filas y 2 columnas)

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:

Formulario con 6 botones - El primero tiene el foco / está seleccionado

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!