Propiedades Anchor y Dock



IronPython - Diseño WinForms: Propiedades Anchor y Dock


En las secciones anteriores hemos hablado sobre Location y Size, las propiedades básicas de los controles winforms para el diseño de la interfaz de nuestras aplicaciones.

También hemos hablado de cómo adaptar la posición y tamaño de los controles cuando cambia el tamaño del formulario. Lo hacemos manejando el evento Resize.

Bien, ha llegado el momento de hablar de las propiedades Anchor y Dock, que en muchos casos pueden facilitarnos la vida enormemente.


Propiedad Anchor


La propiedad Anchor (anclaje) de un control define a que direcciones está anclado el control. El comportamiento que aporta el valor de la propiedad Anchor al control se pone de manifiesto cuando cambia el tamaño del formulario.

El valor de la propiedad Anchor debe ser uno de los definidos en la enumaración System.Windows.Forms.AnchorStyles, o una combinación de estos:

Que un control esté anclado en una dirección significa que si el formulario se redimensiona en esa dirección la posición del control se ajusta para que la distancia entre este y el límite en dicha dirección permanezca constante. Es decir, la propiedad Anchor puede cambiar el valor de la propiedad Location durante un Resize.

El valor de la propiedad Anchor puede ser una combinación de los valores citados anteriormente, unidos a través del operador de disyunción |. De hecho, el valor por defecto de Anchor para todos los controles es AnchorStyles.Top | AnchorStyles.Left, lo que significa que los controles están anclados arriba y a la izquierda por defecto.

Si el valor del Anchor de un control es AnchorStyles.Top | AnchorStyles.Bottom o AnchorStyles.Left | AnchorStyles.Right, el anclaje forzará que cambie el valor de la propiedad Size del control.

Para comprender el funcionamiento de Anchor vamos a crear una aplicación de ejemplo a continuación.


Uso práctico de Anchor

¡¡Vamos a crear un editor de código python!!

Necesitamos un formulario con un RichTextBox en el que escribir código y un Button para ejecutar dicho código. El botón estará en el pie del formulario, en la parte inferior, y la caja de texto ocupará el resto del espacio disponible. ¡¡Manos a la obra!!

El esqueleto de la aplicación es el siguiente:

# -*- coding: utf-8 -*-

import sys, clr

clr.AddReference('System.Windows.Forms')
clr.AddReference('System.Drawing')

from System.Windows.Forms import *
from System.Drawing import *


if sys.implementation.name == 'cpython': # Ajustes para que funcione en PythonNET (no necesarios para IronPython)
    _Size, _Font = Size, Font
    Size = lambda x, y: _Size(int(x), int(y))
    Font = lambda fontfamily, fontsize, fontstyle: _Font(fontfamily, float(fontsize), fontstyle, GraphicsUnit.Pixel)


class MainForm(Form):

    def __init__(self):
        Form.__init__(self)

        self.Text = 'Editor IronPython' if sys.implementation.name == 'ironpython' else 'Editor PythonNET'
        self.Icon = Icon.ExtractAssociatedIcon(sys.executable)
        self.ClientSize = Size(500, 300)
        self.Load += self.set_size_limits
        self.add_controls()

    def set_size_limits(self, *args):
        self.MinimumSize = self.Size
        self.MaximumSize = Size(self.Size.Width*2, self.Size.Height*2)

    def add_controls(self):
        pass

    def ejecutar_codigo(self, *args):
        pass


if __name__ == '__main__':

    Application.EnableVisualStyles()
    Application.Run(MainForm())

Por ahora hemos creado un formulario y hemos definido su texto, icono y tamaño en el método __init__. Cuando el formulario sea cargado se ejecutará el método set_size_limits, donde se asigna un tamaño mínimo y un tamaño máximo para el formulario.

Hemos definido dos métodos vacíos que todavía no hacen nada. En el método add_controls crearemos y añadiremos los controles, y el método ejecutar_codigo será el controlador/manejador del evento Click del botón que vamos a añadir.


Método add_controls

Añadiremos una instancia de RichTextBox que almacenaremos en la variable self.rch:

self.rch = RichTextBox()
self.rch.Location = Point(0, 0)
self.rch.Size = Size(self.ClientSize.Width, self.ClientSize.Height*0.9)
self.rch.Font = Font(FontFamily('Consolas'), 12, FontStyle.Regular)
self.rch.AcceptsTab = True # Permite introducir tabulaciones
self.rch.ForeColor = Color.Blue
self.rch.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top | AnchorStyles.Bottom
self.Controls.Add(self.rch)

Editor IronPython 01

Lo mas importante aquí es lo siguiente:

La caja de texto está posicionada en la esquina superior izquierda. Su ancho es igual al del formulario, y su altura es un 90% la del formulario.

Gracias a la asignación de la propiedad Anchor:

self.rch.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top | AnchorStyles.Bottom

Si cambia el tamaño del formulario también lo hará el de la caja de texto, ya que los 4 límites del formulario están tirando de ella.

Ahora tenemos que añadir un botón que nos permita ejecutar el código escrito en self.rch:

self.btn = Button()
self.btn.Text = 'Ejecutar'
self.btn.Location = Point(0, self.rch.Size.Height)
self.btn.Size = Size(self.ClientSize.Width, self.ClientSize.Height*0.1)
self.btn.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom
self.btn.Click += self.ejecutar_codigo
self.Controls.Add(self.btn)

Editor IronPython 02

Lo mas importante aquí es lo siguiente:

El botón almacenado en self.btn está posicionado totalmente a la izquierda en horizontal. Su posición en vertical coincide con la altura de self.rch. El botón ocupa todo el ancho disponible, y su altura es un 10% de la altura del formulario.

Gracias a la asignación de la propiedad Anchor:

self.btn.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom

Si cambia el tamaño del formulario también lo hará el del botón, ya que el límite de la izquierda, derecha e inferior tiran de él. El botón está anclado en esas 3 direcciones.

Ya hemos registrado el controlador/manejador del evento Click del botón. Ahora tenemos que definir lo que ocurre en él


Método ejecutar_codigo

El código de este método es muy sencillo:

def ejecutar_codigo(self, *args):
    try:
    	exec(self.rch.Text, {})
    except Exception as e:
    	MessageBox.Show(str(e), 'ERROR', MessageBoxButtons.OK, MessageBoxIcon.Error)

Dado que el funcionamiento de este método no es la finalidad de este artículo voy a omitir su explicación. En cualquier caso, seguramente lo comprendes perfectamente.

Editor IronPython 03

Este es el código completo del Editor IronPython:

# -*- coding: utf-8 -*-

import sys, clr

clr.AddReference('System.Windows.Forms')
clr.AddReference('System.Drawing')

from System.Windows.Forms import *
from System.Drawing import *


if sys.implementation.name == 'cpython': # Ajustes para que funcione en PythonNET (no necesarios para IronPython)
    _Size, _Font = Size, Font
    Size = lambda x, y: _Size(int(x), int(y))
    Font = lambda fontfamily, fontsize, fontstyle: _Font(fontfamily, float(fontsize), fontstyle, GraphicsUnit.Pixel)


class MainForm(Form):

    def __init__(self):
        Form.__init__(self)

        self.Text = 'Editor IronPython' if sys.implementation.name == 'ironpython' else 'Editor PythonNET'
        self.Icon = Icon.ExtractAssociatedIcon(sys.executable)
        self.ClientSize = Size(500, 300)
        self.Load += self.set_size_limits
        self.add_controls()

    def set_size_limits(self, *args):
        self.MinimumSize = self.Size
        self.MaximumSize = Size(self.Size.Width*2, self.Size.Height*2)

    def add_controls(self):

        self.rch = RichTextBox()
        self.rch.Location = Point(0, 0)
        self.rch.Size = Size(self.ClientSize.Width, self.ClientSize.Height*0.9)
        self.rch.Font = Font(FontFamily('Consolas'), 12, FontStyle.Regular)
        self.rch.AcceptsTab = True
        self.rch.ForeColor = Color.Blue
        self.rch.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top | AnchorStyles.Bottom
        self.Controls.Add(self.rch)

        self.btn = Button()
        self.btn.Text = 'Ejecutar'
        self.btn.Location = Point(0, self.rch.Size.Height)
        self.btn.Size = Size(self.ClientSize.Width, self.ClientSize.Height*0.1)
        self.btn.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom
        self.btn.Click += self.ejecutar_codigo
        self.Controls.Add(self.btn)

    def ejecutar_codigo(self, *args):
        try:
            exec(self.rch.Text, {})
        except Exception as e:
            MessageBox.Show(str(e), 'ERROR', MessageBoxButtons.OK, MessageBoxIcon.Error)


if __name__ == '__main__':

    Application.EnableVisualStyles()
    Application.Run(MainForm())

Propiedad Dock


La propiedad Dock (acoplamiento) especifica a que borde/s se acopla un control.

El valor de la propiedad Dock debe ser uno de los definidos en la enumaración System.Windows.Forms.DockStyle:

Diferencias entre Anchor y Dock:

DockStyle.None

Es el valor por defecto. El control no es acoplado en ninguna dirección, y su posición y tamaño están definidos por su Location y Size.

DockStyle.None

DockStyle.Left y DockStyle.Right

El control se acopla a la izquierda o a la derecha. Anula a las propiedades Location y Height.

DockStyle.Left

DockStyle.Top y DockStyle.Bottom

El control se acopla arriba o abajo. Anula a las propiedades Location y Width.

DockStyle.Bottom

DockStyle.Fill

El control se acopla en todas las direcciones. Anula a las propiedades Location y Size (tanto Width como Height).

DockStyle.Fill


En el apartado anterior hemos creado un Editor IronPython para ver el funcionamiento de Anchor. Podemos simplificar el método add_controls utilizando Dock y que el programa se comporte exactamente igual.

Nuestro código para add_controls era el siguiente:

def add_controls(self):

    self.rch = RichTextBox()
    self.rch.Location = Point(0, 0)
    self.rch.Size = Size(self.ClientSize.Width, self.ClientSize.Height*0.9)
    self.rch.Font = Font(FontFamily('Consolas'), 12, FontStyle.Regular)
    self.rch.AcceptsTab = True
    self.rch.ForeColor = Color.Blue
    self.rch.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top | AnchorStyles.Bottom
    self.Controls.Add(self.rch)

    self.btn = Button()
    self.btn.Text = 'Ejecutar'
    self.btn.Location = Point(0, self.rch.Size.Height)
    self.btn.Size = Size(self.ClientSize.Width, self.ClientSize.Height*0.1)
    self.btn.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom
    self.btn.Click += self.ejecutar_codigo
    self.Controls.Add(self.btn)

Y podemos simplificarlo utilizando Dock en lugar d Anchor:

def add_controls(self):

    self.rch = RichTextBox()
    self.rch.Font = Font(FontFamily('Consolas'), 12, FontStyle.Regular)
    self.rch.AcceptsTab = True
    self.rch.ForeColor = Color.Blue
    self.rch.Dock = DockStyle.Fill
    self.Controls.Add(self.rch)

    self.btn = Button()
    self.btn.Text = 'Ejecutar'
    self.btn.Height = self.ClientSize.Height*0.1
    self.btn.Dock = DockStyle.Bottom
    self.btn.Click += self.ejecutar_codigo
    self.Controls.Add(self.btn)

Al hacer esta modificación, y una vez compruebes que el programa se comporta exactamente igual, fíjate en lo siguiente:


Evidentemente hay situaciones en las que hay que utilizar Anchor y no podemos optar por Dock. Por ejemplo, cuando un control debe anclarse a la izquierda pero necesitamos un Location.X != 0, o cuando necesitamos anclar en dos direcciones, por ejemplo: AnchorStyles.Left | AnchorStyles.Top.

Espero que te haya quedado claro cómo deben utilizarse estas dos propiedades, que son tremendamente útiles.

Un saludo!