Cómo pintar una tabla en un RichTextBox con Visual Basic .NET

Breve introducción al formato RTF.

Publicado: 23/Agosto/2003
Cipriano Valdezate Sayalero
.

Introducción

La interfaz que ofrece la versión .NET del control RichTextBox está fundamentalmente orientada al texto. Si queremos dibujar las líneas de una tabla o queremos insertar una imagen tenemos que introducir código RTF "a mano". A continuación mostraré una clase que genera un documento que contiene una tabla con su título, pero antes crearemos un sencillo documento en formato RTF que nos servirá de introducción.

Este artículo es una muy somera introducción a la especificación RTF. Si el lector desea profundizar en este inmenso mundo aquí tiene los enlaces a las versiones 1.5 y 1.6.

 

Un sencillo documento RTF

Introducción

La especificación RTF define un conjunto de códigos o "palabras de control" (control words) que se insertan en documentos de texto. Estos códigos, leídos por un lector RTF, son transformados en formato. El control RichTextBox es un lector RTF. Veremos el mínimo código indispensable para aplicar formato al RichTextBox. Algunos de los rasgos que vamos a generar, como el color de fondo del texto o las mismas celdas de una tabla, son imposibles de obtener si no es introduciendo código RTF a mano.

 

El código o la palabra de control (control word)

He aquí, con un ejemplo, su sintaxis:

\trgaph170

Margen izquierdo de una tabla = 170 twips

\

Inicio de la palabra de control

trgaph

Secuencia de caracteres que forman la palabra de control

170

Parámetro

Toda palabra de control se compone de la barra \ seguida de letras minúsculas de la a a la z y un parámetro escrito inmediatamente a continuación, sin paréntesis o corchetes. No todas las palabras de control necesitan parámetro. El fin de la palabra de control y el comienzo de una cadena de texto viene marcado por un espacio que forma parte de la palabra de control y por tanto no se imprime ni aparece en pantalla. Por ejemplo:

\intbl Ven. Dame tu presencia\cell

\intbl marca el inicio de una fila de una tabla, y \cell el final de una celda. Pues bien, el espacio que sigue a la palabra \intbl le indica al lector que lo que viene a continuación ha de ser considerado el texto de la celda, y así será hasta que se tropiece con la barra \, inicio de una palabra de control.

 

El encabezamiento

Todo documento RTF va encerrado en llaves {} y se compone de encabezamiento y documento propiamente dicho.

El encabezado se compone de una línea inicial y una serie de tablas. La sintaxis de la línea inicial es la siguiente: (la letra N mayúscula representa un parámetro numérico. Los corchetes <> encierran una explicación, no pertenecen al código)

\rtfN<declaración de la tabla de caracteres usada>\deffN<declaración del lenguaje utilizado>

El parámetro de \rtf identifica la versión de la especificación RTF utilizada. Aunque se utilice la última versión que es la 1.7 y es la que entiende Word 2003, y mientras no se publique una versión 2.0 y nuestro lector la entienda, este parámetro ha de ser 1.

No nos vamos a molestar en descubrir el resto de los códigos que componen esta línea inicial porque se los vamos a preguntar al RichTextBox y nos va a cantar la línea entera. Antes, sin embargo, veremos las tablas, que también tenemos preguntas que hacerle al respecto.

De las seis tablas que pueden definirse en el encabezado (de fuentes, de archivos, de colores, de estilos, de listas y de marcas de revisión) sólo la de fuentes es obligatoria, y ha de ir la primera. Todas las demás, si aparecen, se colocarán en el orden en que las he enumerado. Nosotros sólo necesitaremos la tabla de fuentes y la de colores.

 

La tabla de fuentes

La tabla de fuentes contiene las definiciones de todas las fuentes utilizadas en el documento. Un cambio de tipo de letra dentro de un documento RTF no se señala con la inserción de la definición de la nueva fuente en el mismo texto, sino con una referencia a la posición que ocupa esa definición en la tabla de fuentes. Igual sucede con los colores que veremos luego.

He aquí un ejemplo de una tabla que contiene dos definiciones de fuentes: (una está pintada de rojo y la otra de verde)

{\fonttbl{\f0\fnil\fcharset0 Verdana;}{\f1\fnil\fcharset0 Microsoft Sans Serif;}}

La tabla entera se encierra entre llaves {}. En primer lugar figura la palabra \fonttbl, y después, también encerradas entre llaves y terminadas por un punto y coma ;, cada una de las definiciones de las fuentes utilizadas en el documento.

Cada fuente se compone de al menos cuatro códigos:

{\f0\fnil\fcharset0 Verdana;}

\fN

Índice de la fuente en la tabla que servirá para referenciarla desde el documento

\fnil

Familia. fnil es el valor por defecto. Otras opciones son \froman, fswiss, \fmodern, etc.

\fcharsetN

Tabla de caracteres para los que está definida la fuente.

Verdana

Nombre de la fuente. No le precede la barra \ sino un espacio

Todo lo que podemos descubrir por nosotros mismos es el índice \fN y el nombre de la fuente. El índice de la tabla de caracteres de cada fuente está recogido en el archivo RTFDEFS.H. Nunca he visto ese archivo. Y aunque lo viera: no sabemos qué tipos de letra tendrá instalados el usuario de nuestro flamante RichTextBox. No nos queda otra opción que someter al RichTextBox a despiadado interrogatorio.

La propiedad .Rtf del control RichTextBox es la vía de entrada y salida del código RTF leído y escrito por él y la condición de posibilidad de este artículo. Todo lo que tenemos que hacer es asignarle un pequeño texto y la fuente que queramos definir. leer su propiedad .Rtf y discriminar lo que nos interese.

Aquí está el código que extrae la primera línea del encabezamiento:

RichTextBox1.Clear()
RichTextBox1.Text= "Cualquier Texto"
RichTextBox1.Font = Form.DefaultFont
Dim t As String = RichTextBox1.Rtf
'A la primera línea le sigue obligatoriamente la tabla de fuentes
Dim z As Integer = t.IndexOf("{\fonttbl{")
PrimeraLinea = t.Substring(0, z)

Y aquí el que extrae la definición de una fuente:

RichTextBox1.Clear()
RichTextBox1.Text= "Cualquier Texto"
RichTextBox1.Font = Form.DefaultFont
Dim t As String = RichTextBox1.Rtf
Dim rr() As String

'Loclaizo el nombre de la fuente y parto el texto en dos
rr = Split(t, f.FontFamily.Name)

'Rastreo la llave izquierda
Dim k1 As Integer = rr(0).LastIndexOf("{")
'y extraigo el fragmanto a partir de ella
Dim s1 As String = rr(0).Substring(k1)

'Rastreo la llave derecha
Dim k2 As Integer = rr(1).IndexOf("}")
'y extraigo el fragmanto hasta ella
Dim s2 As String = rr(1).Substring(0, k2 + 1)

'Uno las tres piezas y ya tengo
'{La cadena completa entre llaves}
Dim s As String = s1 & f.FontFamily.Name & s2

'sustituyo el índice cero 
'por el que será correcto en mi documento
DefiniciónFuente = s.Replace("{\f0\", "{\f" & Indice & "\")

Es conveniente que el RichTextBox al que interrogamos se mantenga lejos de la vista del usuario de nuestra aplicación, que no es cosa de ir por ahí enseñando nuestras miserias.

 

La tabla de colores

Igual que la tabla de fuentes enumera todas las fuentes utilizadas en el documento, la de colores enumera todos los colores. Aquí tenéis un ejemplo de una tabla que enumera los tres colores básicos:

{
\colortbl
\red255\green0\blue0;
\red0\green255\blue0;
\red0\green0\blue255;
}

La palabra introductoria es \colortbl. Las definiciones de los colores no van encerradas entre llaves ni numeradas. El documento se refiere a ellas por su posición en la tabla: la primera es la número 1, la segunda la número 2, etc. No necesito decir que no necesitamos la asistencia del RichTextBox para generarlas. Nos basta una línea de código:

Private Function DefinicionColor(ByVal c As Color) As String
	Return String.Format("\red{0}\green{1}\blue{2};", c.R, c.G, c.B)
End Function

Ya podemos comenzar a escribir nuestro documento RTF: (La especificación RTF, que yo sepa, no admite comentarios, sin embargo los incluiré al estilo Visual Basic)

'Llave de apertura de documento	
{
	
'Línea inicial
\rtf1\ansi\ansicpg1252\deff0\deflang3082

'Tabla de fuentes
{
\fonttbl
{\f0\fnil\fcharset0 Verdana;}
{\f1\fnil\fcharset0 Microsoft Sans Serif;}
}

'Tabla de colores
{
\colortbl
\red255\green0\blue0;
\red0\green255\blue0;
\red0\green0\blue255;
}

Y aquí termina nuestro encabezamiento. Para lo que vamos a hacer no necesitamos más.

 

El documento

Todos los documentos que he generado con el RichTextBox incluyen inmediatemente después del encabezado la siguiente línea:

\viewkind4\uc1

Dejo al ávido lector que consulte su significado en la especificación RTF.

La marca de párrafo es \par. y se coloca al final del párrafo. Podemos ya escribir, por tanto, un documento RTF completo compuesto de un sólo párrafo (pinto el código de azul y dejo el texto de negro) (\'e9 es el código RTF que representa la e con tilde é. No merece la pena detenerse en este detalle, el RichTextBox sabe cómo representar tildes y demás caracteres especiales):

{\rtf1\ansi\ansicpg1252\deff0

{\fonttbl{\f0\fnil\fcharset0 Lucida Console;}}

\viewkind4\uc1


Bueno es saber que los vasos nos sirven para beber; lo malo es que no sabemos para qu\'e9 sirve la sed. Antonio Machado. Campos de Castilla.\par}

El output de este documento es simplemente una línea de texto:

Bueno es saber que los vasos nos sirven para beber; lo malo es que no sabemos para qué sirve la sed. Antonio Machado. Campos de Castilla.

Al no llevar ninguna indicación de formato, el texto toma el tipo de letra definido en primer lugar en la tabla de fuentes y los demás rasgos por defecto, que son aquéllos que hayamos asignado previamente el RichTextBox. Añadiremos poco a poco rasgos al texto para que tenga pinta de poesía

En primer lugar introduciremos los saltos de línea insertando códigos \par y tabularemos las líneas con códigos \tab. Además de tabular la línea también podemos justificarla introduciendo códigos \ql (justificación a la izquierda) \qr (a la derecha) o \qc (línea centrada) al principio de la línea. No los vamos a utilizar aquí, lo dejaremos para que experimente el lector.

\tab Bueno es saber que los vasos\par\tab nos sirven para beber;\par\tab lo malo es que no sabemos\par\tab para qu\'e9 sirve la sed.\par\tab Antonio Machado. Campos de Castilla\par

(Mientras no varíe el encabezado, no lo repito)

Output:

    Bueno es saber que los vasos
    nos sirven para beber;
    lo malo es que no sabemos
    para qué sirve la sed.
    Antonio Machado. Campos de Castilla

Un código \par nace cuando pulsamos la tecla Intro, y cada fragmento de texto terminado por un código \par se considera un párrafo.

A continuación personalizaremos el tipo de letra. Actualizaremos la tabla de fuentes e introduciremos en el texto referencias a las definiciones de la tabla mediante el código \fN, donde N es el índice de la definición. Copio el documento entero:

{\rtf1\ansi\ansicpg1252\deff0\deflang3082

{\fonttbl
{\f0\fnil\fcharset0 Tahoma;}
{\f1\fnil\fcharset0 Courier New;}}

\viewkind4\uc1

\f0\tab Bueno es saber que los vasos\par
\tab nos sirven para beber;\par
\tab lo malo es que no sabemos\par
\tab para qu\'e9 sirve la sed.\par
\tab\f1 Antonio Machado. Campos de Castilla\par}

Output:

Bueno es saber que los vasos
nos sirven para beber;
lo malo es que no sabemos
para qué sirve la sed.

Antonio Machado. Campos de Castilla

Observe mi paciente lector que una marca de formato en un párrafo se transmite a los siguientes. El tipo de letra de índice cero se ha propagado por todos los párrafos hasta que ha tropezado con el índice 1. Esto es porque los párrafos heredan el formato del anterior. Si queremos evitar la herencia y forzar a un párrafo a volver al formato por defecto, lo iniciaremos con la palabra de control \pard. De hecho, el primer párrafo después del encabezado comienza siempre con \pard. Lo reflejaremos en todos los ejemplos a partir de ahora.

Veamos ahora el coloreado del texto:

{\rtf1\ansi\ansicpg1252\deff0\deflang3082

{\fonttbl
{\f0\fnil\fcharset0 Tahoma;}
{\f1\fnil\fcharset0 Courier New;}}

{\colortbl ;\red175\green90\blue20;}

\viewkind4\uc1

\pard\cf1\f0\fs24\tab Bueno es saber que los vasos\par
\tab nos sirven para beber;\par
\tab lo malo es que no sabemos\par
\tab para qu\'e9 sirve la sed.\par
\cf0\f1\tab Antonio Machado. Campos de Castilla\par}

Output:

Bueno es saber que los vasos
nos sirven para beber;
lo malo es que no sabemos
para qué sirve la sed.

Antonio Machado. Campos de Castilla

Dijimos que el color no lleva índice, sino que se referencia mediante su posición en la tabla comenzando por el número 1. Pues bien, el código que inserta una marca de color es \cfN, donde N indica la posición del color en la tabla. Pero si el primer color es el número 1, ¿por qué el último párrafo lleva \cf0? Porque algunas palabras de control admiten el parámetro 0 que actúa como si fuera una etiqueta de cierre. \cf0 fuerza al texto que le sigue a pintarse con el color por defecto en vez de heredar el color del párrafo anterior. Pronto veremos otras palabras de control que admiten parámetro 0.

La palabra de control \fsN establece el tamaño de la fuente medido en "medios puntos". Así, el valor \fs24 indica un tamaño similar al que resultaría de la expresión Font1.Size = 12

En esta tabla presento otros códigos muy habituales con su marca de cierre incluida que añaden formato al documento :

Rasgo

Apertura

Cierre

Negrita

\b

\b0

Cursiva

\i

\i0

Subrayado

\ul

\ul0 o \ulnone

Tachado

\strike

\strike0

Resaltado

\highlightN

\highlight0

El parámetro N de la palabra de control \highlightN apunta a un color de la tabla de colores igual que cfN.

\ul crea un subrayado continuo. El control RichTextBox admite más estilos de subrayado, así que relaciono los códigos a continuación (no los he comprobado todos, quizá alguno no funcione):

Continuo

\ul

Doble

\uldb

Grueso

\ulth

Sólo palabras

\ulw

Ondulado

\ulwave

 

Punto

\uld

Raya

\uldash

Punto Raya

\uldashd

Punto Punto Raya

\uldashdd

Fin de subryado

\ulnone

Recapitulemos todo lo que hemos aprendido y pongámoslo en acción:

 

La tabla

Una tabla es una secuencia de filas, y una fila una secuencia de párrafos separados por marcas de celda. Por ello, la especificación RTF no contiene ninguna palabra de control que inicie y termine una tabla. Con definir los rasgos de las celdas que forman una fila y los propios de la fila queda definida la tabla. Una vez caracterizada, no hay más que repetir la fila introduciendo el texto correspondiente a cada celda para construir la tabla completa.

Así pues, primero indicaremos al RichTextBox que vamos a iniciar una definición de fila insertando el código \trowd. Si hemos escrito texto antes de este código, conviene que lo cerremos con una marca de párrafo.

\trowd va seguido de palabras de control que definen rasgos que afectan a la fila entera. Nosotros incluiremos sólo dos

\tword\trgaph90\trqc

\trgaphN

Espacio entre celdas. Parecido al HTML cellspacing

\trqc

Justificación de la fila: \trqc = central, \trqr: derecha, \trql: izquierda.

El control RichTextBox no admite más estilo de borde que el simple continuo, por consiguiente la palabra de control \trgaphN parece determinar el margen izquierdo del texto dentro de la celda más que la distancia entre los bordes de celdas contiguas. Hay que usarlo, de todos modos, con precaución, porque si exageramos su valor se reduce el espacio para el texto y la fila entera se descoloca.

A continuación debemos definir los bordes superior, izquierdo, inferior y derecho de la fila:

\trbrdrt\brdrs\brdrw10
\trbrdrl\brdrs\brdrw10
\trbrdrb\brdrs\brdrw10
\trbrdrr\brdrs\brdrw10

\trbrdrA, donde A representa la letra t (top), l (left), b (bottom) o r (right), representa el borde superior, izquierdo, inferior y derecho, respectivamente. A continuación figura el código que determina el estilo del borde (\brdrs para borde simple continuo) y su anchura (brdrwN donde N es la anchura en twips) Resulta que el RichTextBox sólo admite el borde simple continuo de 10 twips, aunque le indiquemos otra cosa, de modo que, cualquiera que sea la tabla que dibujemos, estas cuatro líneas nunca varían, así que no voy a detenerme más en ello. El lector encontrará todas las posibilidades que el RichTextBox no admite (o al menos yo no he conseguido que se dibujen) en la especificación RTF.

Definida la fila, definiremos cada una de las celdas:

\clbrdrt\brdrs\brdrw15
\clbrdrl\brdrs\brdrw15
\clbrdrb\brdrs\brdrw15
\clbrdrr\brdrs\brdrw15
\cellx
1890

Estas cinco líneas han de repetirse para cada una de las celdas. Como pueden ver vuesas mercedes, la única diferencia son las dos primeras letras de la primera palabra de control de cada línea: tr para la fila y cl para la celda. Vale para ellas lo mismo que acabo de decir: el control RichTextBox no admite más opciones.

La última línea (\cellxN), sin embargo, sí es única para cada celda, porque establece la posición de su borde derecho. Supongamos que queremos dibujar una fila colocada a 170 twips del margen izquierdo del RichTextBox compuesta de cuatro celdas de 1500, 3000, 1500 y 3000 twips de anchura respectivamente. Entonces el parámetro de la palabra de control \cellxN de cada una de ellas será el siguiente:

  1.  170 + 1500 = 1670
  2. 1670 + 3000 = 4670
  3. 4670 + 1500 = 6170
  4. 6170 + 3000 = 9170

Ya tenemos, ¡por fin!, suficiente información para dibujar las filas de la tabla. Aquí va una fila con un poema de Antonio Machado compuesto de un solo verso:

\intbl Hoy\cell es\cell siempre\cell todavía\cell\row

\intbl

Inicio de la fila

\cell

Fin de celda

\row

Fin de la fila

Dentro de la celda podemos introducir todas las marcas de formato que vimos para el párrafo.

Con todo lo que hemos aprendido ya podemos escribir un documento RTF a mano. He aquí un ejemplo:

' Encabezamiento: línia inicial
{\rtf1\ansi\ansicpg1252\deff0\deflang3082

' Tabla de fuentes
{\fonttbl
{\f0\fnil\fcharset0 Bookman Old Style;}
{\f1\fnil\fcharset0 Microsoft Sans Serif;}}

' Tabla de colores
{\colortbl ;
\red139\green0\blue0;
\red255\green215\blue0;}
' Fin del encabezamiento

' Documento: Línia inicial
\viewkind4\uc1

' Texto
\pard\par\qc\cf1\b\f0\fs24 LA IMAGEN RESPETADA POR EL INCENDIO\par
Primera estrofa\par
\par

' Tabla: línea inicial
\trowd\trgaph0\trqc

' Bordes de la fila
\trbrdrt\brdrs\brdrw10 
\trbrdrl\brdrs\brdrw10 
\trbrdrb\brdrs\brdrw10 
\trbrdrr\brdrs\brdrw10 

' Bordes de la primera celda
\clbrdrt\brdrw15\brdrs
\clbrdrl\brdrw15\brdrs
\clbrdrb\brdrw15\brdrs
\clbrdrr\brdrw15\brdrs 
\cellx2420

' Bordes de la segunda celda
\clbrdrt\brdrw15\brdrs
\clbrdrl\brdrw15\brdrs
\clbrdrb\brdrw15\brdrs
\clbrdrr\brdrw15\brdrs 
\cellx4670

' Bordes de la tercera celda
\clbrdrt\brdrw15\brdrs
\clbrdrl\brdrw15\brdrs
\clbrdrb\brdrw15\brdrs
\clbrdrr\brdrw15\brdrs 
\cellx6920

' Bordes de la cuarta celda
\clbrdrt\brdrw15\brdrs
\clbrdrl\brdrw15\brdrs
\clbrdrb\brdrw15\brdrs
\clbrdrr\brdrw15\brdrs 
\cellx9170

\pard ' Fin de la definición de fila

' Trazado de la filas
\intbl
\highlight2\cf1\b\f1\fs24 San Miguel \cell de la Tumba\cell
es un grand \cell monesterio, \cell\row

\intbl el mar\cell lo cerca todo,\cell elli yace\cell en medio,\cell\row
\intbl el logar\cell perigloso\cell do sufren\cell grand lazerio\cell\row
\intbl los monges\cell que ý viven \cell en essi\cell cimiterio\cell\row

\pard\par ' Fin del trazado de las filas

' Últimos párrafos
\qc\highlight0 Gonzalo de Berceo\par
\ul Milagros de Nuestra Señora\par
} ' Fin de documento RTF

 

El código

El programa que presento genera un documento RTF compuesto de una tabla precedida de su título. Contiene una clase Linea que almacena información sobre una determinada línea del título de la tabla, y una clase Celda que recibe información sobre una celda en particular. Una clase DocumentoRTF recibe un array de objetos Linea que representa todas las líneas del título de la tabla y un array de objetos Celda que representa todas las celdas de una fila. DocumentoRTF recopilará otra información sobre el documento, como el número de filas de la tabla o el margen izquierdo, será entregada a una clase que he llamado Redactor, porque su misión es redactar el documento RTF a la luz de la información contenida en DocumentoRTF. Recibido el documento por el código cliente, se lo entregará al RichTextBox mediante la propiedad .Rtf.

Una vez que el Redactor dispone de toda la información encapsulada en un DocumentoRTF crea un StringBuilder donde irá almacenando mediante su método .Appen(String) todos los fragmentos del documento RTF según los vaya generando. Completado el documento se lo entrega al código cliente en una cadena mediante la función .ToString().

El Redactor admite una matriz de tipo String de tantas filas y tantas columnas como tenga la tabla con la intención de insertar el contenido de la matriz en las celdas ordenadamente. A la hora de crear las celdas el Redactor les introduce un carácter, por ejemplo #, y cuando ha terminado de escribir el documento RTF completo lo fragmenta mediante la función Split a través de ese carácter, y luego vuelve a reunir los trozos intercalando ordenadamente los valores de la matriz:

Dim Fragmentos() As String = Split(ElStringbuilder.ToString, "#")
'El texto situado en el índice 0 no entra en el bucle
'Porque es todo el código previo a la tabla
Dim TextoRTF As String = Fragmentos(0)
Dim Contador As Short
For i As Short = 0 To Matriz.GetUpperBound(0)
    For j As Short = 0 To Matriz.GetUpperBound(1)
          On Error Resume Next
          'Esto es como la función Join()
          'Sólo que en vez de insertar el mismo texto
          'Insertamos ordenadamente los valores de una matriz
          Contador  += 1
          TextoRTF &= Matriz(i, j) & Fragmentos(u)
    Next
Next
Return TextoRTF

Conseguir que las referencias a las tablas de fuentes fueran correctas llegó a complicarse más de lo esperado. Hay que llevar un registro de las definiciones presentes en la tabla y/o descubrir si la nueva definición está ya presente, si lo está, localizar su índice, y si no lo está, añadirla. No es que sea excesivamente complicado, de hecho ese es el proceso que sigo con la tabla de colores; simplemente, no merece la pena. Es totalmente viable añadir una definición a la tabla por cada marca de fuente insertada en el texto, aunque se repitan. Resulta que, si después de entregar al RichTextBox un documento RTF, le preguntamos por ese mismo documento, no nos lo devuelve igual que como se lo dimos. Parece que lo genera a partir del texto formateado generado por el documento RTF que le hemos entregado. Y todas las definiciones repetidas en las tablas desaparecen. No obstante, es curioso que, si nos fijamos bien, descubriremos que el RichTextBox también añade sus propias redundancias.

Hagan vuesas mercedes la prueba si sienten curiosidad: el menú Tabla de mi aplicación de ejemplo ofrece tres opciones:

Construir

RichTextBox1.Rtf = DocumentoRTF : RichTextBox2.Text = DocumentoRTF

Reconstruir

RichTextBox2.Text = RichTextBox1.Rtf

Deconstruir

RichTextBox1.Rtf = RichTextBox2.Text

Generen una tabla y pulsen después en Reconstruir, y verán qué sorpresa.

A continuación copio el código de la clase Redactor. De las clases que constan sólo de propiedades basta con su interfaz:

Public Enum Subrayado
    Continuo
    Punto
    Raya
    PuntoRaya
    PuntoPuntoRaya
    Doble
    Grueso
    Palabras
    Ondulado
End Enum
Public Enum Alineación
    Izquierda
    Centro
    Derecha
End Enum

Public MustInherit Class Formato 
    Public Property Fuente() As Font   
    Public Property ColorTexto() As Color       
    Public Property Subrayado() As Subrayado       
    Public Property ColorFondo() As Color       
End Class
Public Class Linea : Inherits Formato
    Public Property Texto() As String
    Public Property Alineación() As Alineación
End Class
Public Class Celda : Inherits Formato
   Public Property Texto() As String
   Public Property Anchura() As Integer
End Class
Public Class DocumentoRTF 
Public Property Fila() As Celda()
   Public Property Encabezado() As Linea()
   Public Property MargenIzquierdo() As Integer
   Public Property AnchuraTotal() As Integer
   Public Property NumeroFilas() As Integer
End Class
Imports System.Text

Public Class Redactor
  
    Private Albañil As StringBuilder = New StringBuilder
    Private DocRTF As DocumentoRTF
    Private HiddenRichTextBox As RichTextBox
    Private sem As Boolean
    Private Txtt(,) As String

    Public Sub New(ByVal tabla As DocumentoRTF, ByVal RichTextBoxControl As RichTextBox)
        DocRTF = tabla : HiddenRichTextBox = RichTextBoxControl
    End Sub

    Public Function RTF() As String

        'Primera línea del encabezado
        '{\rtf1\ansi\ansicpg1252\deff0\deflang3082\deflangfe3082\deftab708
        '{\fonttbl{
        HiddenRichTextBox.Font = Form.DefaultFont
        Dim t As String = HiddenRichTextBox.Rtf
        Dim z As Integer = t.IndexOf("{\fonttbl{")
        Albañil.Append(t.Substring(0, z))

        'Tabla de fuentes
        Albañil.Append("{\fonttbl")
        Dim ff() As String = FuentesArray()
        For i As Short = 0 To ff.GetUpperBound(0)
            Albañil.Append(ff(i))
        Next
        Albañil.Append("}")

        'Tabla de colores
        Albañil.Append("{\colortbl")
        Dim cc() As Color = ColoresArray()
        For i As Short = 0 To cc.GetUpperBound(0)
            Albañil.Append(ToRTF(cc(i)))
        Next
        Albañil.Append("}")

        'Final encabezado
        Albañil.Append("\viewkind4\uc1")

        'Título
        Dim lin As Linea
        For i As Short = 0 To DocRTF.Encabezado.GetUpperBound(0)
            lin = DocRTF.Encabezado(i)

            'Inicio de párrafo
            Albañil.Append("\pard")

            'Alineación
            Select Case lin.Alineación
                Case Alineación.Centro
                    Albañil.Append("\qc")
                Case Alineación.Derecha
                    Albañil.Append("\qr")
                Case Alineación.Izquierda
                    Albañil.Append("\ql")
            End Select

            'Índice de los colores
            Albañil.Append("\highlight" & Array.IndexOf(cc, lin.ColorFondo))
            Albañil.Append("\cf" & Array.IndexOf(cc, lin.ColorTexto))

            Dim f As Font = lin.Fuente

            'Tachado
            If f.Strikeout = True Then Albañil.Append("\strike") Else Albañil.Append("\strike0")

            'Subrayado
            Albañil.Append(MarcaSubr(f.Underline, lin.Subrayado))

            'Negrita
            If f.Bold = True Then Albañil.Append("\b") Else Albañil.Append("\b0")

            'Cursiva
            If f.Italic = True Then Albañil.Append("\i") Else Albañil.Append("\i0")

            'Índice de la fuente
            Albañil.Append("\f" & i)

            'Tamaño
            Albañil.Append("\fs" & Math.Round(f.Size * 2))

            'Texto
            Albañil.Append(" " & lin.Texto)

            'Final de párrafo 
            Albañil.Append("\par")
        Next
        'Fin del encabezado


        'La tabla

        'Calcularemos las longitudes de cada celda 
        'y el margen izquierdo
        'proporcionales a la anchura total
        Dim LongCeldas(DocRTF.Fila.GetUpperBound(0)) As Integer
        For i As Short = 0 To DocRTF.Fila.GetUpperBound(0)
            LongCeldas(i) = DocRTF.Fila(i).Anchura
        Next

        Dim Longitudes() As Integer = _
        MapeoLongitudes(DocRTF.MargenIzquierdo, LongCeldas, DocRTF.AnchuraTotal)

        'Margen izquierdo
        Albañil.Append(String.Format("\trowd\trqc\trgaph{0}\trleft0", Longitudes(0)))

        'Bordes de la tabla
        Albañil.Append("\trbrdrt\brdrs\brdrw10")
        Albañil.Append("\trbrdrl\brdrs\brdrw10")
        Albañil.Append("\trbrdrb\brdrs\brdrw10")
        Albañil.Append("\trbrdrr\brdrs\brdrw10")

        Dim BordeDerecho As Integer = Longitudes(0)
        For i As Short = 1 To Longitudes.GetUpperBound(0)
            'Bordes de la celda
            Albañil.Append("\clbrdrt\brdrw15\brdrs")
            Albañil.Append("\clbrdrl\brdrw15\brdrs")
            Albañil.Append("\clbrdrb\brdrw15\brdrs")
            Albañil.Append("\clbrdrr\brdrw15\brdrs")

            'Anchura de la celda
            BordeDerecho += Longitudes(i)
            Albañil.Append("\cellx" & BordeDerecho)
        Next

        'Inicio de la tabla
        Albañil.Append("\pard")

        'Como todas las filas van a ser iguales
        'Primero creamos una y luego la fotocopiamos.
        Dim cel As Celda
        Dim Cemento As StringBuilder = New StringBuilder

        'Inicio de fila
        Cemento.Append("\intbl")

        Dim cx, cy As Short
        For i As Short = 0 To DocRTF.Fila.GetUpperBound(0)
            'Cada ciclo del bucle es una celda
            cel = DocRTF.Fila(i)

            'Índice de los colores
            Cemento.Append("\highlight" & Array.IndexOf(cc, cel.ColorFondo))
            Cemento.Append("\cf" & Array.IndexOf(cc, cel.ColorTexto))

            Dim f As Font = cel.Fuente

            'Tachado
            If f.Strikeout = True Then Cemento.Append("\strike") Else Cemento.Append("\strike0")

            'Subrayado
            Cemento.Append(MarcaSubr(f.Underline, cel.Subrayado))

            'Negrita
            If f.Bold = True Then Cemento.Append("\b") Else Cemento.Append("\b0")

            'Cursiva
            If f.Italic = True Then Cemento.Append("\i") Else Cemento.Append("\i0")

            'Tamaño
            Cemento.Append("\fs" & Math.Round(f.Size * 2))

            Cemento.Append("\f" & i + DocRTF.Encabezado.GetUpperBound(0) + 1)

            'Texto
            If sem = False Then
                Cemento.Append(" " & cel.Texto)
            Else
                Cemento.Append(" " & "##")
            End If

            'Final de celda 
            Cemento.Append("\cell")

        Next
        'Final de la fila
        Cemento.Append("\row")

        'Ahora reproducimos las filas
        For i As Short = 1 To DocRTF.NumeroFilas
            Albañil.Append(Cemento.ToString)
        Next

        'Última línea
        Albañil.Append("\pard\par}")

        'Datos tomados de la matriz
        If sem = True Then
            Dim ss() As String = Split(Albañil.ToString, "##")
            Dim st As String = ss(0)
            Dim u As Short
            For i As Short = 0 To Txtt.GetUpperBound(0)
                For j As Short = 0 To Txtt.GetUpperBound(1)
                    On Error Resume Next

                    u += 1
                    st &= Txtt(i, j) & ss(u)

                Next
            Next
            Return st
        Else
            Return Albañil.ToString
        End If

    End Function

    Public WriteOnly Property Texto() As String(,)
        Set(ByVal Value As String(,))
            sem = True
            Txtt = Value
        End Set
    End Property

    Private Function ToRTF(ByVal f As Font, ByVal Indice As Short) As String
        'convierte una fuente en su definición

        HiddenRichTextBox.Font = f

        Dim t As String = HiddenRichTextBox.Rtf

        Dim rr() As String

        'Loclaizo el nombre de la fuente y parto el texto en dos
        rr = Split(t, f.FontFamily.Name)

        'Rastreo la llave izquierda
        Dim k1 As Integer = rr(0).LastIndexOf("{")
        'y extraigo el fragmanto a partir de ella
        Dim s1 As String = rr(0).Substring(k1)

        'Rastreo la llave derecha
        Dim k2 As Integer = rr(1).IndexOf("}")
        'y extraigo el fragmanto hasta ella
        Dim s2 As String = rr(1).Substring(0, k2 + 1)

        'Uno las tres piezas y ya tengo
        '{La cadena completa entre llaves}
        Dim s As String = s1 & f.FontFamily.Name & s2

        'sustituyo el índice cero 
        'por el que será correcto en mi documento
        Return s.Replace("{\f0\", "{\f" & Indice & "\")
    End Function
    Private Function ToRTF(ByVal c As Color) As String
        '\red0\green255\blue255;
        Return String.Format("\red{0}\green{1}\blue{2};", c.R, c.G, c.B)
    End Function

    Private Function FuentesArray() As String()
        Dim lineas() As Linea = DocRTF.Encabezado
        Dim celdas() As Celda = DocRTF.Fila

        'creamos un array en el que quepan todas las fuentes
        Dim rtff(lineas.GetUpperBound(0) + celdas.GetUpperBound(0) + 1) As String

        'Generamos y almacenamos las definiciones de fuentes 
        'de los párrafos del título
        For i As Short = 0 To lineas.GetUpperBound(0)
            rtff(i) = ToRTF(lineas(i).Fuente, i)
        Next

        'Lo mismo con las fuentes de las celdas
        For i As Short = lineas.GetUpperBound(0) + 1 To rtff.GetUpperBound(0)
            rtff(i) = ToRTF(celdas(i - lineas.GetUpperBound(0) - 1).Fuente, i)
        Next

        Return rtff
    End Function
    Private Function ColoresArray() As Color()
        Dim lineas() As Linea = DocRTF.Encabezado
        Dim celdas() As Celda = DocRTF.Fila

        'Esta función genera el array de todos los colores
        'presentes en el objeto DocRTF
        'pero no incluye ningún color repetido

        Dim rr As ArrayList = New ArrayList
        Dim c As Color

        For i As Short = 0 To lineas.GetUpperBound(0)
            c = lineas(i).ColorFondo
            If Array.IndexOf(rr.ToArray(GetType(Color)), c) = -1 Then rr.Add(c)
            c = lineas(i).ColorTexto
            If Array.IndexOf(rr.ToArray(GetType(Color)), c) = -1 Then rr.Add(c)
        Next

        For i As Short = 0 To celdas.GetUpperBound(0)
            c = celdas(i).ColorFondo
            If Array.IndexOf(rr.ToArray(GetType(Color)), c) = -1 Then rr.Add(c)
            c = celdas(i).ColorTexto
            If Array.IndexOf(rr.ToArray(GetType(Color)), c) = -1 Then rr.Add(c)
        Next

        Return rr.ToArray(GetType(Color))
    End Function

    Private Function MapeoLongitudes(ByVal margen As Integer, ByVal celdas As Integer(), ByVal total As Integer) As Integer()
        Dim suma As Integer
        For i As Short = 0 To celdas.GetUpperBound(0)
            suma += celdas(i)
        Next
        Dim res(celdas.GetUpperBound(0) + 1) As Integer
        'Este mapeo calcula las longitudes proporcionalmente 
        'a la anchura total de la tabla

        'margen
        res(0) = total * margen \ suma

        'celdas
        For i As Short = 0 To celdas.GetUpperBound(0)
            res(i + 1) = total * celdas(i) \ suma
        Next

        Return res

    End Function

    Private Function MarcaSubr(ByVal raya As Boolean, ByVal u As Subrayado) As String
        Dim su As String = "\ul"
        If raya = True Then
            Select Case u
                Case Subrayado.Doble : su &= "db"
                Case Subrayado.Grueso : su &= "th"
                Case Subrayado.Ondulado : su &= "wave"
                Case Subrayado.Palabras : su &= "w"
                Case Subrayado.Punto : su &= "d"
                Case Subrayado.PuntoPuntoRaya : su &= "dashdd"
                Case Subrayado.PuntoRaya : su &= "dashd"
                Case Subrayado.Raya : su &= "dash"
            End Select
            Return su
        Else
            Return su & "0"
        End If
    End Function
End Class

 

El código de ejemplo

Además del generador de tablas motivo de este artículo incluyo dos bonuscode: un generador de tablas de caracteres en la fuente que elijamos y un pequeño ejemplo que muestra cómo exportar a una tabla RTF una tabla de una base de datos. Muestro una captura de la tabla de caracteres:


Fichero con el código de ejemplo en Visual Basic .NET 2003 (Tabla RTF - 194 KB)


ir al índice