Alinear textbox verticalmente con API y con C#

Tamaño de letra:

Aplicación de Windows Form. Versión 1.0.2

Ventana con texto centradoCuando programamos en Visual C# una aplicación de Windows Form, un cuadro de texto (textbox) no tiene, por defecto, una propiedad para alinear verticalmente el texto. Para poder alinearlo verticalmente necesitamos primeramente un cuadro de texto multilínea. Hoy se me ha ocurrido una forma sencilla de alinear el texto; te mostraré primero paso a paso cómo lo hice y pensé y, seguidamente, pondré todo el código que he realizado en C# pero que también he adaptado usando API para que puedas usarlo con tu lenguaje favorito (Delphi, VB, C++, ensamblador o el que tú uses en Windows).

Calentando

Primero y gráficamente, ¿qué pretendo hacer con este tutorial? Partiendo del siguiente ejemplo:

Texto del textbox sin alinear verticalmente

Crear un código para llegar a este resultado:

Texto textbox alineado verticalmente

Puedes ver que he dibujado una flecha, a la derecha, indicando exactamente el centro del textbox. En la segunda imagen he seleccionado el texto para que veas -más abajo lo explico- la altura de las letras.

Recordando cuando programaba en ASM usando directamente todas las API de Windows, me vino a la cabeza los mensajes que se pueden enviar con SendMessage a un "Control Edit". Para este tema de centrar el texto verticalmente, me llamaron la atención estos 3 mensajes:

  • EM_GETRECT: Obtiene el rectángulo de formato de un control de edición (cuadro de texto, Rich Edit control). El rectángulo de formato (formatting rectangle) es el rectángulo en el cual el control dibuja el texto y es independiente del tamaño de la ventana del textbox.
  • EM_SETRECT: Define el rectángulo de formato de un control de edición multilínea y redibuja el control.
  • EM_SETRECTNP: Igual que el anterior pero no redibuja el control.

Esto me hizo pensar que si obtengo solamente el rectángulo donde el texto está dibujado y lo desplazo hacia abajo, lo podré centrar ya que es independiente del control textbox. De eso se trata, hice unas simples pruebas y funcionó, sin embargo, me encontré con algunos problemas.

Obtener el rectángulo donde el texto es dibujado

El código mostrado en este tutorial está hecho en C# (que es el que tengo a mano), sin embargo, una parte se hará usando solamente API de Windows que podrás adaptar a tu lenguaje de programación. Está explicado para que todo el mundo lo pueda entender.

Obtener el rectángulo es muy fácil enviando el mensaje EM_GETRECT ya te devuelve una estructura RECT:

 
// Estructura
public struct RECT
{
    public int Left;
    public int Top;
    public int Right;
    public int Bottom;
}
 

SendMessageRefRect(CuadroTexto.Handle, EM_GETRECT, 0, ref rect);

Como puedes ver, he definido SendMessageRefRect para poder enviar una estructura RECT por referencia. Podría haber usado SendMessage directamente y con Marshal.PtrToStructure definir el puntero como una estructura RECT, pero como lo voy a hacer después con GetTextExtentPoint32, así quedan en este tutorial varias formas de hacerlo.

Tal vez pienses que obteniendo el rectángulo del texto ya se puede centrar: no, no se puede porque normalmente este rectángulo es similar de dimensiones al textbox.

Obtener la altura del textbox

Para poder centrar el texto de un textbox se necesita conocer exactamente la altura que vemos del texto en píxeles. Mira esta imagen:

Dimensiones para alinear texto en un textbox

Como se puede apreciar en la imagen, el valor de x es muy fácil de calcular:

x = a - b =
altura del textbox / 2
-
altura del texto / 2
=
(altura del textbox) - (altura del texto) / 2

¿Y cómo se obtiene la altura del texto? Ya viste en el apartado anterior que no vale las dimensiones del rectángulo que contiene el texto. Te voy a mostrar 3 opciones y sus pros y contras:

1.- Sólo para .NET

En C# puedes hacer lo siguiente:

int altoFuente = miTextbox.Font.Height; //Obtiene el interlineado de esa fuente

En mis pruebas, este código no siempre es exacto y esto provoca que el texto no quede siempre perfectamente centrado.

2.- Solo para .NET

En C# puedes hacer lo siguiente:

SizeF altoFuente = TextRenderer.MeasureText("karmany.NET", miTextbox.Font);

El resultado devuelto en SizeF (estructura con dos valores de punto flotante) es el ancho y el alto de la fuente utilizada. En mis pruebas, este resultado ha sido totalmente fiable y si te fijas en la imagen anterior, es exactamente el tamaño -por fila- del recuadro azul. En el ejemplo de la imagen de arriba, el alto de la fuente serían 17 px.

Pensando de esta forma es posible que, lógicamente, se te ocurra hacer lo siguiente:

SizeF altoTotalFuente = TextRenderer.MeasureText(miTextbox.Text, miTextbox.Font);

pero este código no siempre funcionará. ¿Por qué?

En un textbox tienes la propiedad WordWrap que permite o no, saltar a una nueva línea. En la imagen anterior, el comienzo del texto es exactamente este: "En un lugar de la mancha de cuyo nombre no quiero acordarme" y está escrito en UNA sola línea, pero como tengo la propiedad WordWrap = true, el texto se va generando automáticamente en nuevas líneas.
Esto significa que el código anterior te dirá que la altura de ese texto es 17 px pero eso no es cierto porque, como ves, son 3 líneas = 17 x 3 = 51 píxeles.

3.- Usando API de Windows

Esta forma me ha dado también un buen resultado y se puede utilizar con cualquier lenguaje de programación, simplemente adaptándolo en las llamadas a las funciones. La forma de calcular el tamaño de una fuente se puede realizar con la API GetTextExtentPoint32. Esto ya lo escribí hace 7 años al centrar texto en un listbox usando VB6 (ver función calculatamaño). Yo voy a intentar "traducir" esa función a C#:

 
private int AlturaDeFuente(TextBox txt)
        {
            // Obtiene el dispositivo de contexto del textbox:
            IntPtr HDC = GetDC(CuadroTexto.Handle);
            // Microsoft dice que si falla retorna null, pero
            // hago pruebas y retorna 0, por eso:
            if (HDC == IntPtr.Zero) return 0;
 
            // Obtiene la fuente del textbox:
            IntPtr hfuente = SendMessage(CuadroTexto.Handle, WM_GETFONT,
                                         IntPtr.Zero, IntPtr.Zero);
 
            // Selecciona la fuente dentro del dispositivo de contexto:
            IntPtr Lfuente = SelectObject(HDC, hfuente);
 
            // Calcula el tamaño del texto,
            // ptr es un puntero a la estructura Size: sz.
            string texto = "karmany.NET"; //esto es muy importante
            IntPtr ptr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Size)));
            bool resultado = GetTextExtentPoint32(HDC, texto, texto.Length, ptr);
            if (!resultado) return 0;
 
            Size sz;
            try
            {
                sz = (Size)Marshal.PtrToStructure(ptr, typeof(Size));
            }
            catch
            {
                return 0;
            }
            finally
            {
                // Libera memoria
                Marshal.FreeHGlobal(ptr);
                SelectObject(HDC, Lfuente);
                ReleaseDC(CuadroTexto.Handle, HDC);
            }
            return sz.Height;
        }
 

Protejo Marshal.PtrToStructure porque podría generar una excepción. Cuando la función ReleaseDC libera el dispositivo de contexto, retorna 1.

Ya tengo el alto de una fuente, ahora hay que calcular cuánto mide exactamente el texto escrito en el textbox. Para esto simplemente necesitamos calcular cuántas líneas hay escritas. Esto se hace muy fácilmente con SendMessage y el mensaje EM_GETLINECOUNT:

En una aplicación de Windows Form, un cuadro de texto (textbox) no tiene, por defecto, la propiedad LineCount. Sin embargo, LineCount sí que existe para Windows Presentation Foundation (WPF).

var numeroLineas = SendMessage(CuadroTexto.Handle, EM_GETLINECOUNT, IntPtr.Zero, IntPtr.Zero);
int numeroDeLineas = numeroLineas.ToInt32();

numeroDeLineas retornará un valor Entero con el número de líneas que vemos en un textbox, NO es el número de líneas reales. Mira, el texto real del textbox es este:

En un lugar de la mancha de cuyo nombre no quiero acordarme,
vivía karmany.NET
con su fiel escudero...

Pero en el textbox vemos 5 líneas.

Calcular la altura del texto es simplemente: (número de líneas) x (altura de la fuente). En este caso: 5 líneas x 17 px = 85 píxeles. Y, como se explicó en la imagen anterior, se calcula la posición top del rectángulo que contiene el texto con la fórmula ya vista:

// Calcula el valor top de rect:
rect.Top = (miTextbox.Height - (int)hLinea.Height) / 2;

Observaciones

Antes de ponerte todo el código completo verás que he puesto:

rect.Top -= 2;

Este valor lo he puesto "a ojo" y da un resultado perfectamente alineado. ¿por qué? Tengo mis dudas, es posible que haya un margen entre el área cliente del textbox y el rectángulo de formato.

Los últimos problemas

A la hora de centrar, he tenido en cuenta cuándo el texto ocupa más ancho del textbox. En este caso se me ha ocurrido poner barra de desplazamiento vertical, devolver el tamaño original del rectángulo de formato y restarle 17 píxeles a la derecha que son los que ocupa la barra de desplazamiento:

// El texto no cabe en el textbox:
CuadroTexto.ScrollBars = ScrollBars.Vertical;
rect = rectInicial;
rect.Right -= 17;

Si utilizas el código visto hasta ahora y en el evento del textbox TextChanged llamas a AlinearTextoTextboxVerticalmente, verás que el texto desaparece y esto es porque los valores del rectángulo de formato se van modificando automáticamente. Te pongo los resultados de la estructura rect devuelta por EM_GETRECT:

1ª- Left:5 Top:1 Right:216 Bottom:153
2ª- Left:6 Top:35 Right:215 Bottom:149
3ª- Left:7 Top:35 Right:214 Bottom:130
4ª- Left:8 Top:35 Right:213 Bottom:111
...

Como puedes observar, el valor Bottom desciende demasiado y hace que el texto desaparezca. Para evitar este problema relleno una estructura RECT (rectInicial) que guardará los valores y los volverá a establecer con EM_SETRECT: (creo que la comparación bottom es buena)

// Obtiene la primera estructura RECT inicial si no existe:
if (rectInicial.Bottom == 0) SendMessageRefRect(CuadroTexto.Handle, EM_GETRECT, 0,ref rectInicial);

Para aprovechar todo el área de escritura y que quede perfectamente alineado, verás tras este código se pone el valor top = 1 y el valor bottom = (ancho del texto - 1).

El código

Está hecho para C#, debes crear un nuevo proyecto y una aplicación de Windows Form, dibuja un control textbox y ponle de nombre: CuadroTexto. Después agrega este código:

 
// v1.0.2
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Runtime.InteropServices;
 
namespace CentrarTextbox
{
    public partial class Form1 : Form
    {
        // Estructura
        public struct RECT
        {
            public int Left;
            public int Top;
            public int Right;
            public int Bottom;
 
            public override string ToString()
            { 
                return String.Format("Left = {0} Top = {1} Right = {2} Bottom = {3}",
                                      Left, Top, Right, Bottom);
            }
        }
 
        // RECT inicial:
        RECT rectInicial = new RECT();
 
        // Constantes:
        const int EM_GETRECT = 0x00B2;
        const int EM_SETRECT = 0x00B3; // Sí redibuja el control.
        const int EM_SETRECTNP = 0x00B4; // No redibuja el control.
        const int EM_GETLINECOUNT = 0x00BA;
        const int WM_GETFONT = 0x0031;
 
        // API SendMessage:
        [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
        private static extern int SendMessageRefRect(IntPtr hWnd, UInt32 Msg,
                                                     int wParam, ref RECT rect);
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        private static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg,
                                                 IntPtr wParam, IntPtr lParam);
 
        // API's para obtener alto de fuente:
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        private static extern IntPtr GetDC(IntPtr hWnd);
        [DllImport("gdi32.dll", CharSet = CharSet.Auto)]
        private static extern IntPtr SelectObject(IntPtr hDc, IntPtr hObject);
        [DllImport("gdi32.dll", CharSet = CharSet.Auto)]
        private static extern bool GetTextExtentPoint32(IntPtr hdc, string lpString,
                                                        int cbString, IntPtr size);
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
 
        public Form1()
        {
            InitializeComponent();
        }
 
        private void Form1_Load(object sender, EventArgs e)
        {
            // Alinea el texto de textbox verticalmente:
            AlinearTextoTextboxVerticalmente();
        }
 
        private void AlinearTextoTextboxVerticalmente()
        {
            // Obtiene el rectángulo de formato, donde va realmente
            // escrito el texto:
            RECT rect = new RECT();
            SendMessageRefRect(CuadroTexto.Handle, EM_GETRECT, 0, ref rect);
 
            // Obtiene la primera estructura RECT inicial si no existe:
            if (rectInicial.Bottom == 0)
            {
                SendMessageRefRect(CuadroTexto.Handle, EM_GETRECT, 0, ref rectInicial);
                // Aprovecha el máximo de área:
                rectInicial.Top = 1;
                rectInicial.Bottom = CuadroTexto.Height - 1;
            }
 
            // Calcula el número de líneas escritas en el textbox:
            int numeroDeLineas = LineasTextbox(CuadroTexto);
 
            #region Calcula el alto de la fuente de texto de 2 formas:
            SizeF hLinea = new SizeF();
            // 1.- Solo para .NET:
            hLinea = TextRenderer.MeasureText("karmany.NET", CuadroTexto.Font);
 
            // 2.- Con API:
            //hLinea.Height = AlturaDeFuente(CuadroTexto);
            #endregion
 
            // Calcula el alto total del texto en px:
            hLinea.Height = hLinea.Height * numeroDeLineas;
 
            // Comprueba si el texto sobrepasa el tamaño:
            if (hLinea.Height > (CuadroTexto.Height))
            {
                // El texto no cabe en el textbox:
                CuadroTexto.ScrollBars = ScrollBars.Vertical;
                rect = rectInicial;
                rect.Right -= 17;  
            }
            else
            {
                CuadroTexto.ScrollBars = ScrollBars.None;
                // Se centra según fórmula explicada:
                rect.Top = (CuadroTexto.Height - (int)hLinea.Height) / 2;
                rect.Top -= 2;
                rect.Bottom = rectInicial.Bottom;
                rect.Left = rectInicial.Left;
                rect.Right = rectInicial.Right;
            }
 
 
            // Centra el texto y redibuja el textbox:
            SendMessageRefRect(CuadroTexto.Handle, EM_SETRECT, 0, ref rect);
        }
 
        private int AlturaDeFuente(TextBox txt)
        {
            // Obtiene el dispositivo de contexto del textbox:
            IntPtr HDC = GetDC(CuadroTexto.Handle);
            // Microsoft dice que si falla retorna null, pero
            // hago pruebas y retorna 0, por eso:
            if (HDC == IntPtr.Zero) return 0;
 
            // Obtiene la fuente del textbox:
            IntPtr hfuente = SendMessage(CuadroTexto.Handle, WM_GETFONT,
                                         IntPtr.Zero, IntPtr.Zero);
 
            // Selecciona la fuente dentro del dispositivo de contexto:
            IntPtr Lfuente = SelectObject(HDC, hfuente);
 
            // Calcula el tamaño del texto,
            // ptr es un puntero a la estructura Size: sz.
            string texto = "karmany.NET"; //esto es muy importante
            IntPtr ptr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Size)));
            bool resultado = GetTextExtentPoint32(HDC, texto, texto.Length, ptr);
            if (!resultado) return 0;
 
            Size sz;
            try
            {
                sz = (Size)Marshal.PtrToStructure(ptr, typeof(Size));
            }
            catch
            {
                return 0;
            }
            finally
            {
                // Libera memoria
                Marshal.FreeHGlobal(ptr);
                SelectObject(HDC, Lfuente);
                ReleaseDC(CuadroTexto.Handle, HDC);
            }
            return sz.Height;
        }
 
        private int LineasTextbox (TextBox txt)
        {
            var NumeroLineas = SendMessage(txt.Handle, EM_GETLINECOUNT,
                                           IntPtr.Zero, IntPtr.Zero);
            return NumeroLineas.ToInt32();
        }
 
        private void CuadroTexto_TextChanged(object sender, EventArgs e)
        {
            // El texto se alinea mientras escribes
            AlinearTextoTextboxVerticalmente();
        } 
    }
}
 

La verdad que he tardado bastante en crear este tutorial y hacer pruebas. Ahora mismo estoy importando este código en un proyecto propio y... ¡funciona perfecto! Esto puede ser muy útil en la presentación de formularios antes de una impresión, ya que al imprimir, sí que se puede alinear verticalmente. Me resulta extraño que todavía no haya una propiedad TextVerticalAlign o algo así.

Si quieres que siempre esté centrado, deberás alinearlo verticalmente con cada evento TextChanged del textbox, observa también que el ámbito es principalmente privado (private) por si lo quieres utilizar en otro módulo. El tamaño de altura ideal de un textbox es un valor (tal vez) múltiplo de la altura de fuente para que muestre todas las líneas antes de que salga la barra de desplazamiento.

Versiones

1.0.1 - 25 de agosto de 2013

Primera versión

1.0.2 - 27 de agosto de 2013

Al calcular la estructura inicial RECT, se modifican los valores top y bottom aprovechando todo el alto del textbox para que quede perfectamente centrada y el texto ocupe todo el alto del control.

Última actualización: Martes, 27 Agosto 2013
Comentarios  
0 # Wilmen Valles 31-10-2021 14:28
Gracias por el Aporte, muy pocas personas tratan de ayudar a los que tenemos poco conocimiento de la programación, estoy interesado en el Tema por cuanto tengo un problema en tratar de centrar un control texboxt en un control panel pero que cuando cambie la propiedad Font, o el tamaño del texto escrito se ajuste, asi como tambien si el Tamaño del Panel cambia.
0 # Wilmen Valles 31-10-2021 14:08
Muy Interesante, pero me gustaria ver como se aplica, no tengo mucho conocimiento de programacion.
+1 # Fabricio Leite 09-05-2015 19:32
Artigo muito bom!
Bem completo, me ajudou.
Obrigado!

No tiene privilegios para responder a los comentarios.


 
Visitas: 8487950