Autor: Cathy Pountney
Original en inglés: Visual FoxPro 9.0 Report Writer In Action
http://msdn.microsoft.com/library/en-us/dnfoxgen9/html/VFP9Reports2.asp
Traducido por: Ana María Bisbé York
Se aplica a: Visual FoxPro 9.0
Clase ReportListener abstracta
Creamos una clase nueva llamada basada MyReportListener en la clase base de VFP 90 ReportListener. Esta clase va a contener algunos métodos que son necesarios en algunas clases ReportListener que se desarrollaran en el futuro. Por consiguiente, es mejor crear una clase abstracta tal que todas las clases ReportListener restantes sean subclases de ella. Será necesario agregar código en los métodos Init(), BeforeBand(), y crear un nuevo método GetFRXRecord().
Método Init()
En el método Init() de la clase abstracta ReportListener, aprovechamos dos clases que encontramos en la carpeta HOME() + 'FFC'. Estas clases se utilizarán posteriormente y ahorrarán mucho tiempo y código.
*-- MyReportListener::Init() DODEFAULT() *-- Crear un objeto gráfico para usar después This.AddProperty('ogpGraphics', ; NEWOBJECT('gpGraphics', HOME() + 'FFC\_GDIPlus')) *-- Crear un objeto FRXCursor para usar después This.AddProperty('oFRXCursor', ; NEWOBJECT('frxCursor', HOME() + 'FFC\_FRXCursor'))
En el método BeforeBand(), agregar código para establecer el controlador gráfico utilizado en la llamada al GDI+.
*-- MyReportListener::BeforeBand() LPARAMETERS nBandObjCode, nFRXRecno DODEFAULT(nBandObjCode, nFRXRecNo) * Debido a que el controlador GDI+ cambia en cada página, * es necesario configurar el controlador para nuestro objeto GPGraphics. This.ogpGraphics.SetHandle(This.GDIPlusGraphics)
Luego, crear un método nuevo.
Método GetFRXRecord()
Existen varias ocasiones donde es necesario referenciar el registro FRX del objeto a imprimir. Creamos un método nuevo, llamado GetFRXRecord(), que pase a la sesión de datos apropiada, ponga los datos del registro FRX en un objeto, restablezca la sesión de datos, y luego, devuelva el objeto que hace la llamada (the caller)
*-- MyReportListener::GetFRXRecord() LPARAMETERS pnFRXRecNo LOCAL lnSession, loFRX *-- Establecer la nueva sesión de datos - el FRX lnSession = SET("Datasession") SET DATASESSION TO This.FRXDataSession *-- Ir al registro GOTO pnFRXRecNo *-- Tomar los datos SCATTER MEMO NAME loFRX *-- Restablecer la sesión de datos SET DATASESSION TO lnSession *-- Devolver el dato RETURN loFRX
Clase ReportListener_Directives
Creamos una clase nueva, llamada MyReportListener_Directives, basada en la clase abstracta MyReportListener. Luego, agregamos algunas propiedades nuevas y código a algunos de sus métodos.
Propiedades
Agregamos siete propiedades nuevas a la clase:
- aFRXRecords[1]: Este arreglo unidimensional puede ser eventualmente redimensionado con la misma cantidad de números de filas que coinciden con el total de números de registro en la tabla FRX. Para cada registro con directivas, se crea una colección (collection) en el registro que guarda la referencia a cada clase directiva que necesita.
- cDelimiter (predeterminado ||): Esta propiedad es utilizada para identificar el carácter delimitador utilizado en la directiva del campo de un objeto de informe. El delimitador puede cambiarse con facilidad, al agregar este dato a una propiedad en lugar de escribir código duro.
Precaución: No utilice el delimitador que sea común a los comandos y expresiones de VFP. Por ejemplo, no utilice la coma. - lRender (predeterminado .t.): Esta propiedad puede ser utilizada, en caso de ser necesario, para suprimir la generación actual del objeto.
- nAdjustHeight (predeterminado 0): Esta propiedad puede ser utilizada para manipular la altura de un objeto durante el método Render().
- nAdjustLeft (predeterminado 0): Esta propiedad puede ser utilizada para manipular el extremo izquierdo de un objeto durante el método Render().
- nAdjustTop (predeterminado 0): Esta propiedad puede ser utilizada para manipular el extremo superior durante el método Render().
- nAdjustWidth (predeterminado 0): Esta propiedad puede ser utilizada para manipular el ancho de un objeto durante el método Render().
Ahora que están definidas las propiedades, creamos algunos métodos nuevos y agregamos código a algunos métodos existentes.
Método GetFRXDirectives()
Creamos un nuevo método, llamado GetFRXDirectives(). Este método recupera todas las directivas para un registro dado del FRX, analiza la información, crea un objeto collection, y luego instancia la clase directiva correspondiente para cada directiva. El nombre de la clase directiva de usuario para instanciar se deriva de la directiva en el FRX. A la colección se le agrega una referencia de objeto a la clase directiva, junto con cualquier parámetro adicional que siga al nombre de la directiva.
*-- MyReportListener_Directives::GetFRXDirectives() *-- Toma las directivas del objeto en el FRX LPARAMETERS tnFRXRecNo LOCAL loFRX, ; lnLines, ; laLines, ; ln, ; lcText, ; loObj, ; llSuccess, ; lnSession, ; lnWordCount, ; lnWord *-- Toma el registro FRX loFRX = This.GetFRXRecord(tnFRXRecNo) *-- Establece la sesión de datos para que la directiva de objetos *-- sea creada en la misma sesión de datos que el informe lnSession = SET("Datasession") SET DATASESSION TO This.CurrentDataSession *-- Procesa el campo USER buscando las directivas DIMENSION laLines[1] lnLines = ALINES(laLines, loFRX.USER) IF lnLines > 0 FOR ln = 1 TO lnLines lnWordCount = GETWORDCOUNT(laLines[ln], This.cDelimiter) IF lnWordCount >= 2 AND ; GETWORDNUM(laLines[ln], 1, This.cDelimiter) == '*:LISTENER' *-- Agrega una colección (collection) al arreglo *-- en caso de que no esté IF VARTYPE(This.aFRXRecords[tnFRXRecNo]) <> 'O' This.aFRXRecords[tnFRXRecNo] = CREATEOBJECT('Collection') ENDIF *-- Procesa la directiva y trata de agregar el objeto lcDirective = GETWORDNUM(laLines[ln], 2, This.cDelimiter) llSuccess = .t. TRY This.aFRXRecords[tnFRXRecNo].Add( ; CREATEOBJECT('MyDirectives_' + lcDirective, This)) CATCH llSuccess = .f. ENDTRY IF NOT llSuccess LOOP ENDIF *-- Crea el objeto loObj = This.aFRXRecords[tnFRXRecNo] *-- Analiza los parámetros IF lnWordCount >= 3 DIMENSION loObj[loObj.Count].aParameters[lnWordCount-2] FOR lnWord = 3 TO lnWordCount loObj[loObj.Count].aParameters[lnWord-2] = ; GETWORDNUM(laLines[ln], lnWord, This.cDelimiter) ENDFOR ENDIF *-- Procesa cualquier código adicional en el INIT de la directiva loObj[loObj.Count].AdditionalInit() ENDIF ENDFOR *-- Si existe una colección (collection) se deshace de ella IF VARTYPE(This.aFRXRecords[tnFRXRecNo]) = 'O' AND ; This.aFRXRecords[tnFRXRecNo].Count = 0 This.aFRXRecords[tnFRXRecNo] = .f. ENDIF ENDIF *-- Restablece la sesión de datos SET DATASESSION TO lnSession
Método BeforeReport()
Después de definir el método GetFRXDirectives(), agregamos el método BeforeReport() para calcular cuántos registros hay en el FRX, la dimensión de la nueva matriz de propiedades, y llamar al método GetFRXDirectives() para cada registro FRX.
*-- MyReportListener_Directives::BeforeReport() LOCAL lnSession *-- Conectar con el FRX lnSession = SET('DATASESSION') SET DATASESSION TO This.FRXDataSession *-- Crear una matriz DIMENSION This.aFRXRecords[RECCOUNT()] STORE .f. TO This.aFRXRecords *-- Busca las directivas GOTO TOP SCAN This.GetFRXDirectives(RECNO()) ENDSCAN *-- Restablece la sesión de datos SET DATASESSION TO lnSession
Método AdjustObjectSize()
El siguiente método que necesita código es el método AdjustObjectSize(). Varias directivas necesitan realizar acciones especiales en este método, por tanto, este método necesita un método correspondiente en la clase directiva de usuario para realizar el procesamiento especial.
*-- MyReportListener_Directives::AdjustObjectSize() LPARAMETERS tnFRXRecno, toObjProperties LOCAL loObj, lcExec, loFRX *-- Si es aplicable, procesa la directiva IF VARTYPE(This.aFRXRecords[tnFRXRecNo]) <> 'L' *-- Toma el dato FRX loFRX = This.GetFRXRecord(tnFRXRecNo) FOR EACH loObj IN This.aFRXRecords[tnFRXRecNo] loObj.DoAdjustObjectSize(tnFRXRecNo, toObjProperties, loFRX) ENDFOR ENDIF
Método EvaluateContents()
El siguiente método que necesita código es el método EvaluateContents().Varias directivas necesitan realizar acciones especiales en este método, por tanto, este método necesita un método correspondiente en la clase directiva de usuario para realizar el procesamiento especial.
*-- MyReportListener_Directives::EvaluateContents() LPARAMETERS tnFRXRecno, toObjProperties LOCAL loObj, lcExec, loFRX *-- Si es aplicable, procesa la directiva IF VARTYPE(This.aFRXRecords[tnFRXRecNo]) <> 'L' *-- Toma el dato FRX loFRX = This.GetFRXRecord(tnFRXRecNo) FOR EACH loObj IN This.aFRXRecords[tnFRXRecNo] loObj.DoEvaluateContents(tnFRXRecNo, toObjProperties, loFRX) ENDFOR ENDIF
Método Render()
Varias directivas necesitan realizar acciones especiales en el método Render(). Sin embargo no es tan fácil como en el método EvaluateContents(). La generación no puede hacerse por la clase directiva de usuario. Debe hacerse por este método. Por tanto, este método llama al método DoBeforeRender() en la clase directiva de usuario, luego genera el objeto si es necesario, entonces llama al método DoAfterRender() de la clase directiva de usuario. Es muy importante observar que en este método se utiliza NODEFAULT para que el objeto no se genere dos veces.
*-- MyReportListener_Directives::Render() LPARAMETERS tnFRXRecno, tnLeft, tnTop, ; tnWidth, tnHeight, ; tnObjectContinuationType, ; tcContentsToBeRendered, tGDIPlusImage LOCAL loObj *-- Si es aplicable, procesa la directiva IF VARTYPE(This.aFRXRecords[tnFRXRecNo])<>'L' *-- Ejecuta el código de BeforeRender FOR EACH loObj IN This.aFRXRecords[tnFRXRecNo] loObj.DoBeforeRender( ; tnFRXRecno, tnLeft, tnTop, ; tnWidth, tnHeight, ; tnObjectContinuationType, ; tcContentsToBeRendered, ; tGDIPlusImage) ENDFOR *-- Ahora genera el objeto IF This.lRender ReportListener::Render(tnFRXRecno, ; tnLeft + This.nAdjustLeft, ; tnTop + This.nAdjustTop, ; tnWidth + This.nAdjustWidth, ; tnHeight + This.nAdjustHeight, ; tnObjectContinuationType, ; tcContentsToBeRendered, ; tGDIPlusImage) ENDIF *-- Ejecuta el código de AfterRender FOR EACH loObj IN This.aFRXRecords[tnFRXRecNo] loObj.DoAfterRender( ; tnFRXRecno, tnLeft, tnTop, ; tnWidth, tnHeight, ; tnObjectContinuationType, ; tcContentsToBeRendered, ; tGDIPlusImage) ENDFOR *-- Suprime el comportamiento normal NODEFAULT ENDIF
Clase directiva abstracta
Después de crear la clase MyReportListener_Directives, el siguiente paso es crear la clase directiva de usuario. Es entonces donde se ejecutan todos los procesos especiales. Creamos una clase nueva llamada MyDirectives, y la basamos en la clase base Custom de VFP. Luego, agregamos algunas propiedades y agregamos código en algunos métodos.
Propiedades
Es necesario añadir tres propiedades a la clase abstracta.
- aParameters[1]: Esta matriz guarda los parámetros que hay que aplicar en la directiva. Por ejemplo, si el campo USER de un informe contiene *:LISTENER||ROTATETEXT||-90, esta propiedad contiene una fila con un valor de -90. Esta matriz podría contener filas y valores adicionales, si se hubiesen indicado otros parámetros.
- nSaveGraphicsHandle (predeterminado 0): Contiene con conjunto de valores para llamadas especiales al GDI+. Toma el conjunto en el método DoBeforeRender()y lo utiliza en el método DoAfterRender() to reset things
- oListener:Contiene una referencia de objetos hacia atrás al objeto ReportListener.
Método Init()
El método Init() obtiene una referencia pasada al objeto ReportListener. Este método necesita guardar esta referencia en la propiedad oListener para utilizarla posteriormente en otro método.
*-- MyDirectives::Init() LPARAMETERS toListener *-- Rcordar el objeto Listener This.oListener = toListener
Método ConvertFontStyleToCodes()
El método EvaluateContents() permite cambiar las propiedades de fuente. Sin embargo, referencia estilos de fuente con valores numéricos en lugar de una cadena de código de estilos de fuente. Para controlar esta conversión, creamos un método nuevo, llamado ConvertFontStyleToCodes().
*-- MyDirectives::ConvertFontStyleToCodes() *-- Converir FontStyle a partir de valores numéricos *-- a código carácter LPARAMETERS tnFontStyle LOCAL lcStyle lcStyle = '' IF BITTEST(tnFontStyle, 0) lcStyle = lcStyle + 'B' ENDIF IF BITTEST(tnFontStyle, 1) lcStyle = lcStyle + 'I' ENDIF IF BITTEST(tnFontStyle, 2) lcStyle = lcStyle + 'U' ENDIF IF BITTEST(tnFontStyle, 7) lcStyle = lcStyle + 'S' ENDIF IF EMPTY(lcStyle) lcStyle = 'N' ENDIF RETURN lcStyle
Método ConvertRGBToGDI()
Cuando trabajamos con GDI+, referencia colores de forma diferente a como lo hace VFP. Por tanto, para hacer la conversión, es necesario crear un método llamado ConvertRGBToGDI(), en esta clase abstracta.
*-- MyDirectives::ConvertRGBToGDI() LPARAMETERS tnAlpha, tnRed, tnGreen, tnBlue RETURN (0x1000000 * tnAlpha) + ; (0x10000 * tnRed) + ; (0x100 * tnGreen) + ; tnBlue
Método Empty
En la clase abstracta es necesario crear algunos métodos que serán llamados por la clase ReportListener. En el nivel abstracto no es necesario ningún código, solamente una instrucción para los parámetros en cuatro de ellos. Utilice la siguiente definición de parámetros para cada uno de ellos.
- DoAdjustObjectSize()
LPARAMETERS tnFRXRecno, toObjProperties, toFRX
- DoAfterRender()
LPARAMETERS tnFRXRecNo, tnLeft, tnTop, ; tnWidth, tnHeight, ; tnObjectContinuationType, ; tcContentsToBeRendered, ; tGDIPlusImage
- DoBeforeRender()
LPARAMETERS tnFRXRecNo, tnLeft, tnTop, ; tnWidth, tnHeight, ; tnObjectContinuationType, ; tcContentsToBeRendered, ; tGDIPlusImage
- DoEvaluateContents()
LPARAMETERS tnFRXRecNo, toObjProperties, toFRX
- AdditionalInit()
*-- No son necesarios parámetros – este método permanecerá vacío
No todas las directivas de todos estos métodos; pero es necesario tenerlas definidas en el nivel abstracto, para que el ReportListener pueda hacer las llamadas, aunque no hagan nada. La clase directiva abstracta ya está creada y se pueden crear ahora las clases directivas de forma tal que sean subclases de la clase abstracta. Estas son las clases que realmente procesan una directiva dada.
Clase Directiva RedNegative
Para hacer que los números negativos aparezcan en rojo, se crea una clase nueva, llamada MyDirectives_RedNegative. Esta clase se basa en la clase directiva abstracta y se le agrega el código al método DoEvaluateContents().
DoEvaluateContents()
Este método comienza evaluando la expresión a imprimir. Entonces, cambia el color de la pluma a rojo en caso que el valor sea negativo. Entonces cambia la propiedad ReLoad para decirle a VFP que algo ha cambiado y VFP necesita recargar las propiedades antes de generar.
*-- MyDirectives_RedNegative::DoEvaluateContents() *-- Utiliza rojo si el valor es negativo, *-- en caso contrario, utiliza negro LPARAMETERS tnFRXRecNo, toObjProperties, toFRX LOCAL llNegative DODEFAULT(tnFRXRecNo, toObjProperties, toFRX) *-- ¿Es negativo? TRY llNegative = (VAL(toObjProperties.Text) < 0) CATCH llNegative = .f. ENDTRY *-- Asignar el color IF llNegative toObjProperties.PenRed = 255 toObjProperties.PenGreen = 0 toObjProperties.PenBlue = 0 toObjProperties.Reload = .T. ENDIF
Para utilizar esta directiva, escriba:
*:LISTENER||REDNEGATIVE
en el campo USER de cualquier objeto al que se le pueda aplicar.
Clase Directiva SqueezeText
Para reducir el tamaño de un texto para hacer que quepa en el área definida, creamos una nueva clase directiva, llamada MyDirectives_SqueezeText. Esta clase se basa en la clase directiva abstracta y se le agrega el código al método DoEvaluateContents().
DoEvaluateContents()
Este método verifica si el texto a imprimir cabe en el área definida. Si no cabe, el tamaño de la fuente se reduce en uno y se verifica nuevamente. La verificación se mantiene hasta que encuentre una fuente que permita que quepa todo el texto en el área definida. Se respeta un mínimo de fuente de 4 para evitar que el texto impreso sea demasiado pequeño; pero puede ser sobrescrito con un parámetro en esta directiva.
*-- MyDirectives_SqueezeText::DoEvaluateContents() *-- Si no cabe el texto, asignamos un tamaño más pequeño LPARAMETERS tnFRXRecNo, toObjProperties, toFRX DODEFAULT(tnFRXRecNo, toObjProperties, toFRX) LOCAL lnSmallest, lnFontSize, lnWidth, ; lnMaxWidth, lcText, lnDecimals, ; lcStyle *-- ¿Cuál es la fuente más pequeña para la comprobación? *-- (predeterminado 4) lnSmallest = IIF(EMPTY(This.aParameters[1]), ; 4, VAL(This.aParameters[1])) *-- Preparamos condiciones para el lazo lcStyle = This.ConvertFontStyleToCodes( ; toObjProperties.FontStyle) lnMaxWidth = toFRX.Width && In FRUs lnFontSize = toObjProperties.FontSize lnDecimals = SET("Decimals") SET DECIMALS TO 3 *-- Agrega un carácter extra para asegurarse de que cabe. *-- En caso contrario, pudiera quedar demasiado justo *-- y no caber correctamente lcText = toObjProperties.Text + 'X' *-- Si es necesario, cambia la fuente DO WHILE .t. *-- Si es la fuente más pequeña, sale del lazo IF lnFontSize <= lnSmallest EXIT ENDIF *-- Al utilizar lnFontSize *-- ¿Cuán ancho será el texto(en FRUs)? lnWidth = This.oListener.oFRXCursor.GetFRUTextWidth(lcText, ; toObjProperties.FontName, lnFontSize, lcStyle ) *-- Si el texto cabe con esta letra, *-- sale del lazo IF lnWidth <= lnMaxWidth EXIT ENDIF *-- Reduce el tamaño de fuente lnFontSize = lnFontSize-1 ENDDO *-- Si es necesario, cambia la fuente IF toObjProperties.FontSize <> lnFontSize toObjProperties.FontSize = lnFontSize toObjProperties.Reload = .T. ENDIF *-- Restablece los decimales SET DECIMALS TO &lnDecimals
Para utilizar esta directiva, escriba
*:LISTENER||SQUEEZETEXT
en el campo USER de cualquier objeto al que se le pueda aplicar. El tamaño mínimo de la fuente ya está generado en la clase directiva. Para forzar un tamaño mínimo diferente, se pasa como parámetro a continuación de la directiva. Por ejemplo:
*:LISTENER||SQUEEZETEXT||6
fuerza un tamaño mínimo de fuente igual a 6.
Clase Directiva RotateText
Para rotar un texto, creamos una directiva nueva, llamada MyDirectives_RotateText. Esta clase se basa en la clase directiva abstracta y se le agrega el código a los métodos DoBeforeRender() y DoAfterRender().
Método DoBeforeRender()
El método DoBeforeRender() toma el valor de rotación desde un parámetro de la directiva. Entonces, realiza varias llamadas a las clases GDI+ para establecer los valores necesarios en el método Render() en la clase ReportListener.
*-- MyDirectives_RotateText::DoBeforeRender() *-- Rotar texto LPARAMETERS tnFRXRecNo, tnLeft, tnTop, ; tnWidth, tnHeight, ; tnObjectContinuationType, ; tcContentsToBeRendered, ; tGDIPlusImage DODEFAULT(tnFRXRecNo, tnLeft, tnTop, ; tnWidth, tnHeight, ; tnObjectContinuationType, ; tcContentsToBeRendered, ; tGDIPlusImage) LOCAL lnX, ; lnY, ; lnRotate, ; lnHandle *-- ¿Qué es la rotación? lnRotate = VAL(This.aParameters[1]) *-- Rotar si es necesario IF lnRotate <> 0 * Tomar la version adecuada de coordenadas lnX = tnLeft lnY = tnTop * guarder el estado actual del controlador gráfico lnHandle = 0 This.oListener.ogpGraphics.Save(@lnHandle) This.nSaveGraphicsHandle = lnHandle * Ahora, movemos el punto 0,0 hasta donde queremos de tal forma * que al rotar, lo hacemos alrededor del punto adecuado This.oListener.ogpGraphics.TranslateTransform(lnX, lnY, 0) * Ahora cambiamos el ángulo en e que ocurre el dibujo This.oListener.ogpGraphics.RotateTransform(lnRotate, 0) * Restauramos el punto 0,0 This.oListener.ogpGraphics.TranslateTransform(-lnX, -lnY, 0) ENDIF
Método DoAfterRender()
Después de que el objeto es generado, hay que ejecutar el método DoAfterRender(). En este método se restablece la configuración GDI+ para que el objeto siguiente se genere correctamente. En caso contrario, todo el resto de objetos del informe quedan rotados de igual forma.
*-- MyDirectives_RotateText::DoAfterRender() *-- Rotar texto LPARAMETERS tnFRXRecNo, tnLeft, tnTop, ; tnWidth, tnHeight, ; tnObjectContinuationType, ; tcContentsToBeRendered, ; tGDIPlusImage DODEFAULT(tnFRXRecNo, tnLeft, tnTop, ; tnWidth, tnHeight, ; tnObjectContinuationType, ; tcContentsToBeRendered, ; tGDIPlusImage) * Devolver el estado del controlador gráfico This.oListener.ogpGraphics.Restore(This.nSaveGraphicsHandle)
Para utilizar esta directiva, escriba:
*:LISTENER||ROTATETEXT||nnn
en el campo USER de cualquier objeto al que se le pueda aplicar, donde nnn representa los grados de rotación. Un número positivo rota en sentido de las manecillas del reloj, un número negativo rota en contra de ellas. Por ejemplo:
*:LISTENER||ROTATETEXT||-90
rota el texto 90 grados, contrario a las manecillas del reloj.
Crear un informe
Para utilizar estas tres directivas, cree un informe Nuevo o modifique un informe existente.
Directiva RedNegative
Añada la siguiente directiva al campo USER de uno de los campos numéricos del informe:
*:LISTENER||REDNEGATIVE
Directiva SqueezeText
Añada la siguiente directiva al campo USER, de un campo texto que tiene mucha información, como es un campo descripción. Asegúrese de que el campo es más corto que algunas de las descripciones más largas:
*:LISTENER||SQUEEZETEXT
Directiva RotateText
Añada la siguiente directiva al campo USER de uno de los objetos que será rotado:
*:LISTENER||ROTATETEXT||-90
Ejecutar el informe
Está definida La clase ReportListener, todas las clases directivas están definidas, así que lo único que resta por hacer es ejecutar el informe. Utilice el siguiente código para instanciar la clase ReportListener y mostrar previamente el informe.
SET CLASSLIB TO MyReportListeners ox=NEWOBJECT('MyReportListener_Directives', 'MyReportListeners') ox.ListenerType = 1 && Preview REPORT FORM accounts OBJECT ox
Continua en: El Generador de informes de VFP 9.0 en acción - Parte 3
No hay comentarios. :
Publicar un comentario
Los comentarios son moderados, por lo que pueden demorar varias horas para su publicación.