Su IP: 18.97.9.170
En este artículo se disecciona un script que agrega un par de controles al formulario de Mercancías.
El formulario de mercancías (en lo sucesivo nos referiremos a él como «frmVINV»), permite tratar un artículo de inventario y un producto como si se trataran de una misma entidad. El valor de registro "usarVINV" determina si la creación de nuevos códigos de producto o de inventario se ejecutará empleando este formulario, forzando así una relación uno a uno (1:1) entre cada articulo y producto o viceversa.
Esta solución se diseñó para un distribuidor de licores y bebidas alcohólicas. La Ley requiere que las facturas especifiquen el grado alcohólico y la cantidad total de litros de cada bebida alcohólica vendida. Y para poder hacerlo, es necesario que en algún lugar de la BD exista un espacio para almacenar el contenido por unidad y el grado alcohólico de cada producto para el cual aplique.
Para esos fines, ClearLight incluye los atributos.
Un atributo es una combinación de un identificador -el nombre de la propiedad que el atributo representa, por ejemplo "GRADO", o "VOLUMEN"- con el valor correspondiente a esa propiedad.
El problema particular del que nos ocupamos en este artículo seguramente se habría resuelto de una manera mucho más simple usando una DLL de forma (la única "complicación" en este script es justamente el manejo de los eventos de los controles agregados).
La gran ventaja de las DLL es que al estar escritas en Visual Basic permiten controlar todos los eventos de todos los controles que agreguemos al formulario. Su desventaja es que hay que copiarlas y registrarlas en las máquinas de todos los usuarios que deban tener acceso a la funcionalidad agregada. Los scripts, por otra parte, pueden resultar en código más complejo, pero basta con actualizar posExt.vbs en la carpeta de datos común (a menos que haya buenas razones para tener copias locales del archivo en algunos equipos) para que todos los terminales tengan acceso inmediato a la nueva funcionalidad. Otro punto a favor del uso de scripts es que no requieren que el integrador o el cliente tengan instalado VB6.
Una limitación que siempre nos molestó en el uso de scripts era justamente su incapacidad para responder a los eventos de los controles agregados por los scripts. El usuario de CleaLight espera que al pasar el foco a un control, la barra de status le dé alguna indicación acerca de lo que se espera que escriba en ese control. Si no tenemos la manera de asociar el código necesario con el evento GotFocus de dicho control, no habrá manera de darle esa "pista" al usuario. Igualmente, para validar el contenido de un control es necesario esperar a que el usuario intente registrar la operación transcrita para poder realizar la validación. Y los usuarios de ClearLight esperan que las validaciones se ejecuten inmediatamente despues de aceptar el contenido de cada control.
Hace un par de días escribimos EventosScript.dll, una DLL que permite, justamente, controlar ciertos eventos de los controles más frecuentemente usados en ClearLight. La DLL fue incorporada al paquete de instalación de ClearLight, a partir de la versión 6.7.25, pero es independiente de la versión de ClearLight. Si la necesita ahora, puede descargarla aquí. Y aquí puede descargar el código fuente, en caso de que necesite agregar algún tipo de control o ampliar la gama de eventos a los que se puede responder.
La solución está implementada en posExt.vbs. Veamos ahora, paso a paso, como funciona.
Option Explicit
Private procEvents
Private strBuff
procEvents es una referencia a la clase EventosScript.ProcesadorEventosScript, contenida en EventosScript.DLL.. Se declara a nivel del script para evitar la creación y destrucción repetitiva de instancias, ya que el MsScriptControl causa fugas de memoria en cada destrucción.
Private Sub nl(s)
strBuff.Append CStr(s)
strBuff.Append vbCrLf
End Sub
nl es una pequeña rutina que acumula texto en un bufer de cadena (CStringBuffer), agregando un salto de línea después de cada concatenación.
Uno de los "puntos débiles" de VB6 es su lentitud en la
creación de cadenas (el mismo punto debilita a Java y a los lenguajes .NET,
con el agravante de que no se trata sólo de las adenas sino de todos los
objetos del sistema).
Si escribimos:
Dim cad As String
cad = "Esto es la primera parte de la cadena"
cad = cad & ", y esto es la segunda."
Visual Basic crea espacio para almacenar el valor inicial de cad.
Luego crea espacio adicional para almacenar la cadena temporal originada por
la concatenación. Finalmente copia la dirección de la nueva cadena a la
variable cad.
La memoria originalmente asignada a cad, no es
inmediatamente devuelta al pool de memoria disponible: queda "ocupada" hasta
que el sistema se queda corto de memoria; en ese momento se iniciará un
proceso de "recolección de basura", penosamente lento.
Esto no causa problemas si la aplicación es pequeña. Pero en una aplicación
grande, diseñada para correr por muchas horas seguidas en cada sesión, que
además genera texto dinámico continuamente (como el ensamblado de las
sentencias SQL, o la generación de código adaptativo) puede acabar siendo un
problema bien molesto.
Para evitar esas complicaciones, se creó la clase CStringBuffer, y se
recomienda su uso para cualquier operación que requiera la concatenación de
cadenas de caracteres, tanto en VB6 como en VBScript.
Private
labelGrado
Private labelContenido
Private numGradoAlc
Private numContenido
Variables para hacer referencia a los controles creados por el script.
Public Sub frmVINV_frmVINV_Load(r1, r2)
' Si no se han creado los objetos globales del
script
' los crea. Sólo se ejecuta una vez:
If isEmpty(procEvents)
Then
Set strBuff =
CreateObject("VSLIB.CStringBuffer")
Set procEvents =
CreateObject _
("EventosScript.ProcesadorEventosScript")
procEvents.SetFactoria Factoria
End If
' Creamos la etiqueta para identificar el
control para
' la lectura del grado alcoholico
Set labelGrado = r1.Controls.Add("VB.Label", "lblGrado")
With labelGrado
.Caption = "&Grado:"
.Autosize =
True
.Visible = True
.Left = 8805
.Top
= 1860
.TabIndex = r1.txCantidadEmpaque.TabIndex
+ 1
End With
' Creamos el control para a lectura del
grado alcohólico
Set numGradoAlc = r1.Controls.Add
_
("ucNumero.ucNumBox", "numGradoAlc")
With numGradoAlc
.Formato = "#0.0"
.ShowCalc = 0
.Height = 315
.Width = 750
.Visible =
True
.Enabled = True
.TabStop = True
.TabIndex = labelGrado.TabIndex
+ 1
.Top = 1830
.Left =
9600
End With
' Código de control de eventos para
numGradoAlc
strBuff.Value = ""
' se inicia el buffer en blanco
nl "Public Sub GotFocus(p)"
nl "p.Parent.StatusBar1.Panels(1).Text =
""Introduzca el grado alcohólico del producto"""
nl "End Sub"
nl "Public Sub Validate(p)"
nl " If p.Value <= 0 And p.Enabled Then"
nl " MsgBox ""Debe indicar un grado
alcohólico positivo"", vbCritical, ""Invalido"""
nl " isValid = False"
nl " Exit Sub"
nl " End If"
nl "End Sub"
' registra el control con el procesador de eventos
del script
procEvents.AddControl numGradoAlc, "ctlGrado", strBuff.Value
' etiqueta para identificar el control para la
lectura del volumen
Set labelContenido = r1.Controls.Add("VB.Label", "lblContenido")
With labelContenido
.Caption = "Contenido:"
.Autosize = True
.Visible = True
.Left = 8805
.Top = 2265
.TabIndex = numGradoAlc.TabIndex
+ 1
End With
' control para la lectura del volumen
Set numContenido = r1.Controls.Add
_
("ucNumero.ucNumBox", "numContenido")
With numContenido
.Formato = "#0.000"
.ShowCalc = 0
.Height = 315
.Width = 750
.Visible =
True
.Enabled = True
.TabStop = True
.TabIndex = labelContenido.TabIndex + 1
.Top = 2235
.Left
= 9600
End With
' codigo para el control de eventos de
numGradoAlc
strBuff.Value = ""
nl "Public Sub GotFocus(p)"
nl " p.Parent.StatusBar1.Panels(1).Text =
""Introduzca el contenido por envase del producto"""
nl "End Sub"
nl "Public Sub Validate(p)"
nl " If p.Value <= 0 And p.Enabled Then"
nl " MsgBox ""Debe indicar un contenido
positivo"", vbCritical, ""Invalido"""
nl " isValid = False"
nl " Exit Sub"
nl " End If"
nl "End Sub"
' registra el control cpn el procesador de
eventos
procEvents.AddControl numContenido, "ctlContenido", strBuff.Value
End Sub
La rutina anterior se ejecuta cada vez que se carga una instancia de frmVINV. Y es en ella donde está casi toda la complicación (y la hermosura) de la técnica para agregar controles a los formularios desde posExt.
El metodo Add de la coleccion Controls de los formularios de Visual Basic permite agregar al formulario controles creados dinámicamente.
La sintaxis es la siguiente:
Set c = formulario.Controls.Add(<nombreClase>,
<nombreControl>, <contenedor>)
El nombre de la clase puede obtenerse fácilmente si se abre un formulario
creado con VB6 con un editor de texto. Por lo general, los controles
intrínsecos de VB6 llevan un nombre formado por el identificador VB (el
nombre de la librería o "espacio de nombres" que los contiene) seguido de un
punto, seguido del "nombre familiar" del control. En nuestro código tenemos
un ejemplo en VB.Label.
Los controles de terceros (incluidos los nuestros) llevan igualmente el
nombre de la librería seguido de un punto, seguido del nombre de la clase
del control. Si Ud. dispone de VB6, puede crear un proyecto, agregar un
formulario y agregar a éste una instancia del control cuyo nombre quiere
conocer. Guarde el formulario, ábralo con un editor de texto (Notepad.exe,
el bloc de notas de Windows, sirve a falta de algo mejor) y observe el
identificador de tipo con el que está asociado.
Cuando se agrega un control dinámico a un formulario, su posición inicial es (0, 0) y su estado es inactivo e invisible.
Despues de agregar un control es necesario posicionarlo correctamente en el formulario (ajustando sus propiedades Top y Left), hacerlo visible -si es procedente- asignarle una posición dentro del orden de tabulación (asignandole el valor correcto a la propiedad TabIndex: en nuestro ejemplo, queremos que el grado alcohólico y el contenido sean editados despues de la cantidad de unidades por empaque, por eso le asignamos al primer control el valor siguiente a éste, y luego el valor siguiente a cada uno de los tres controles sucesivos).
Lo más tedioso, si no se tiene el código fuente del formulario, es determinar la posición de los controles.
Public Sub frmVinv_frmVinv_SetContexto(r1, r2)
Select Case r1.Contexto
Case 0, 1
numContenido.Enabled = False
numGradoAlc.Enabled = False
If r1.Contexto
= 0 Then
numContenido.Value = 0
numGradoAlc.Value = 0
End If
Case 2, 3
' Los controles solo son editables
si se trata
' de una bebida alcohólica
numContenido.Enabled = r1.txLinea.Text
= "ALC"
numGradoAlc.Enabled =
r1.txLinea.Text
= "ALC"
End Select
End Sub
Las FIE tienen un Contexto, que indica qué operaciones
pueden realizarse sobre ellas.
El Contexto 0 sólo permite cerrar el formulario o seleccionar una nueva
entidad: no hay datos cargados. Cuando el contexto es 0, los valores deben
ser igualmente 0, ya que no se aplican a nada.
El Contexto 1 permite realizar consultas u operaciones sobre la entidad
presentada, comenzar la edición de sus datos descriptivos, eliminarla o
regresar al contexto 0 para selecionar una nueva entidad.
El Contexto 2 activa la edicion.
El Contexto 3 es similar al 2, pero sólo se activa cuando la forma se ha
abierto en modo de Creacion de una nueva entidad.
Queremos que nuestros controles sean editables sólo cuando se trate de una
bebida alcohólica (identificadas mediante el valor "ALC" en su línea).
Public Sub frmVINV_txLinea_LostFocus(f, c)
If c.Text = "ALC"
Then
Enable (numContenido)
Enable (numGradoAlc)
Else
Disable (numContenido)
numContenido.Value = 0
Disable (numGradoAlc)
numGradoAlc.Value= 0
End If
End Sub
Cuando el valor de la línea cambia para indicar que se trata de una bebida alcohólica, se activan los controles (Enable y Disable son métodos de VSLIB que activan o desactivan el control referenciado por su argumento, que se pasa entre paréntesis para eitar conflictos entre VBScript y Visual Basic: de lo contrario se produciría un error 13: incompatibilidad de tipos).
' Carga los valores de los atributos
Public Sub frmVINV_frmVinv_InstanceLoaded(f, c)
Dim rs
Set rs = OpenRecordset( _
"SELECT CodigoAtributo, ValorAtributo " & _
"FROM Atributos
" & _
"WHERE TipoEntidad = 'INV' " & _
"AND CodigoEntidad = ? " & _
"AND CodigoAtributo IN
('GRADO', 'VOLUMEN')",
_
f.txCodigo.Text)
numContenido.Value = 0
numGradoAlc.Value = 0
Do While Not rs.EOF
On Error Resume Next
If rs(0) = "GRADO"
Then
numGradoAlc.Value = CDbl(rs(1))
ElseIf rs(0) = "VOLUMEN"
Then
numContenido.Value = CDbl(rs(1))
End If
On Error Goto 0
rs.MoveNext
Loop
rs.Close
End Sub
InstanceLoaded es la última adición a la lista de notificaciones que los formularios envían a CUsrDLL (y por delegación a posExt). Es llamada despues de que los datos de una entidad han sido cargados en los controles corespondientes del formulario. Normalmente esto ocurre en la misma rutina donde el contexto es puesto a 1 (entidad válida cargada). Pero SetContexto puede ser llamada antes o despues de que los datos sean presentados en el formulario, mientras que InstanceLoaded siempre es llamada despues de la carga, y podemos estar seguros de que los valores contenidos en los controles son actuales.
En todas las FIE, el segundo argumento de la llamada a InstanceLoaded es la instancia de la entidad cuyos datos acaban de ser presentados. En frmVINV, que es la que nos ocupa ahora, esto no ocurre, porque los VINV son la combinación de dos clases diferentes (clsItemVenta y clsItemInventario) y no hay ninguna razón para preferir uno antes que el otro.
' Actualiza los atributos
Public Sub frmVINV_clsItemVenta_Save(f, i)
Dim elItem
Set elItem = f.pItemInventario
If f.txLinea.Text
= "ALC" Then
elItem.SetValorAtributo "GRADO", numGradoAlc.Value
elItem.SetValorAtributo "VOLUMEN", numContenido.Value
Else
elItem.SetValorAtributo
"GRADO"
elItem.SetValorAtributo
"VOLUMEN"
End If
End Sub
El evento Save es generado por las FIE cuando se ha actualizado o creado un nuevo registro de entidad. En nuestro caso, si es una bebida alcohólica, actualizamos los atributos GRADO y VOLUMEN. De lo contrario, los eliminamos.
Ahora llegó el momento de analizar la manera para definir el código de respuesta a los eventos de los controles.
EventosScript, en su versión estandar actual, permite responder a los cuatro eventos más comunes de los controles: GotFocus (el control se activa), Validate (el usuario seleciona otro control, y se quiere validar el contenido del actual, o procesar algún efecto adicional), KeyDown (se pulsa cualquier tecla, incluso una tecla de función o de control) y KeyPress (se pulsa una tecla "de valor", o Enter).
Para agregar un control al procesador de eventos, se llama al método AddControl. La sintaxis es la siguiente:
Public Sub AddControl(elControl, classID, Codigo)
Donde:
elControl es el control cuyos eventos queremos
controlar.
classID es el nombre del contenedor de código. En cada
procesador de eventos no puede haber dos classID iguales. Si se intenta, el
código para los eventos del control con el classID repetido serán el
definido para el primer control, y
codigo: el código de los manejadores de evento,
formateado como una cadena de VB.
El código de los manejadores de evento es código normal de VBScript, "empaquetado" dentro de una cadena. Eso le da esa apariencia tan odiosa.
Por ejemplo, para el evento GotFocus de numContenido, queremos que la barra de status del formulario le diga al operador que debe introducir el contenido por envase del producto.
Normalmente escribiríamos esto:
Public Sub GotFocus(p)
p.Parent.StatusBar1.Panels(1).Text =
"Introduzca el contenido por envase del producto"
End Sub
Y para validar que el contenido no sea cero, escribiríamos
esto:
Public Sub Validate(p)
If p.Value <= 0 And p.Enabled Then
MsgBox "Debe indicar un contenido
positivo", vbCritical, "Invalido"
isValid = False
Exit Sub
End If
End Sub
Pero tenemos que meter todo el código en una sola cadena, entonces usamos la funcion nl para agregar automáticamente el salto de línea -requerido por VBScript al final de cada instrucción- y para ahorrarnos la llamada "calificada" a strBuff.Append.
En cada llamada a nl, debemos delimitar cada línea de código con comillas (si no, VBScript no entenderá que se trata de una cadena), y usar dos comillas como delimitadores de las cadenas internas del código (por ejemplo "Debe indicar un contenido positivo").
Entonces, el resultado final es esto:
strBuff.Value = ""
nl "Public Sub GotFocus(p)"
nl " p.Parent.StatusBar1.Panels(1).Text =
""Introduzca el contenido por envase del producto"""
nl "End Sub"
nl "Public Sub Validate(p)"
nl " If p.Value <= 0 And p.Enabled Then"
nl " MsgBox ""Debe indicar un contenido
positivo"", vbCritical, ""Invalido"""
nl " isValid = False"
nl " Exit Sub"
nl " End If"
nl "End Sub"
Por ultimo, agregamos el control al procesador de eventos:
procEvents.AddControl numContenido, "ctlContenido", strBuff.Value
Puede haber soluciones menos "retorcidas": colocar todo el código en filas de una base de datos, o en un archivo de texto, leerlo en una cadena y pasar la cadena completa. Pero todas ellas implican la existencia de un elemento adicional, y eso por lo general trae más problemas que el esfuerzo requerido para convetir código en "metacódigo", que es de lo que se trata esto.