Logo

ClearLight
Sistema Integrado para la Administracion

Su IP: 18.97.9.170



Como agregar controles a un formulario usando VBScript

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.