Cómo pintar una tabla en un RichTextBox con Visual Basic .NETBreve introducción al formato RTF.Publicado: 23/Agosto/2003
|
. |
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 FunctionYa 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\uc1Dejo 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 CastillaUn 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 CastillaObserve 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 CastillaDijimos 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
\cellx1890Estas 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:
- 170 + 1500 = 1670
- 1670 + 3000 = 4670
- 4670 + 1500 = 6170
- 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 TextoRTFConseguir 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 ClassImports 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)