Acerca de:

Este blog contiene los códigos, ejemplos y bases de datos que he usado cuando aprendía acerca de algún tema específico. En lugar de borrarlos (una vez dominado ya el tema), he decidido publicarlos :)
Mostrando entradas con la etiqueta VB6. Mostrar todas las entradas
Mostrando entradas con la etiqueta VB6. Mostrar todas las entradas

jueves, 31 de julio de 2025

Intercambiando Punteros entre Visual Basic 6 y Borland C++

Otro tutorial de los viejitos, de casi veinte años atrás, así que sirve como documento histórico :D

Ya que hemos podido pasar un parámetro desde una aplicación en Visual Basic a una Dll compilada con Borland C++ y devolver a Visual Basic el resultado calculado desde la Dll, lo que hace falta ahora es hacer lo mismo con arrays. 
No se puede enviar todo un vector desde Visual Basic a una Dll, lo que se puede hacer al operar con arrays es llamar a la función en la Dll dentro de un bucle que recorra el array en el programa de Visual Basic, pero si se desea que nuestra aplicación sea más rápida es mejor dejarle el manejo de los bucles a C++. En este caso se le enviaría a la Dll la dirección de memoria donde está el array (es decir: la dirección de su primer elemento). 

Un excelente tutorial sobre los punteros en C++ está akí: 

En contra de lo que muchos creen, Visual Basic sí maneja punteros. Más información se encuentra en esta web: http://www.thevbzone.com/secrets.htm 
Pero para este caso no se necesitan estas funciones, basta pasarle a la Dll el primer elemento del array tal y como lo explican en esta otra web: https://recursosvisualbasic.com.ar/rvb/htm/tutoriales/interaccion-visual-basic-c++.htm

Hay que tener cuidado con un par de detalles que no mencionan estas páginas y que explicaré más adelante.
En resumen, lo que se quiere es que la Dll programada en C++ reciba y devuelva un puntero. 
Para una Dll que simplemente recibe un puntero y devuelve un dato el código es:

#include <windows.h>
#include "puntero.h"

BOOL WINAPI DllEntryPoint(HINSTANCE, DWORD, LPVOID)
{
  return TRUE;
}

extern "C"
long __declspec (dllexport) WINAPI sumatotal (long *puntero, int valor) {

  long sumatoria;
  sumatoria = 0;

  for (int i = 0; i < valor; i++)
    sumatoria= sumatoria+ puntero[i];

  return sumatoria;

}

Y el archivo .h es:

#ifndef _PUNTERO_H
#define _PUNTERO_H

#ifdef BUILD_DLL // en la construcción de la librería
#define EXPORT __declspec(dllexport)
#else // en la construcción del ejecutable
#define EXPORT __declspec(dllimport)
#endif

#ifdef __cplusplus /* if in a C++ program */
extern "C"
#endif

long __declspec (dllexport) WINAPI sumatotal (long *puntero, int valor);

#endif 

Para probarlos se compilan y se copia la Dll (llamada "puntero.dll") en la carpeta System32. Luego se abre en el Visual Basic un proyecto exe estándar y se ingresa el siguiente código:

Private Sub Form_Load()
  Dim c As Long, i As Integer
  Dim mat() As Long

  ReDim mat(0 To 5)

  For i = 0 To UBound(mat)
     mat(i) = 2 * i
  Next

  c = sumatotal(mat(0), UBound(mat) + 1)
  Form1.Caption = c

End Sub

Y en un módulo .bas se coloca:

Public Declare Function sumatotal Lib "puntero.dll" (ByRef puntero As Long, ByVal valor As Integer) As Long

"Byref" le dice a Visual Basic que lo que va a enviar es un puntero, no una variable.
En este ejemplo se nota la diferencia en el manejo de los índices entre Visual Basic y C++. Los índices del array "mat" son 0, 1, 2, 3, 4, 5. Si se hubiera creado en C++, para que tuviera seis elementos, se habría definido como mat[6] siendo sus índices también 0, 1, 2, 3, 4, y 5 ya que en C++ el número entre corchetes define la cantidad de elementos del array. Debido a la costumbre programé el bucle para tratar al array como si hubiera sido creado en C++, cuando en realidad es un array de Visual Basic en el cual el 5 representa el índice mayor mas no el número de elementos, por ello se le suma uno al segundo parámetro al enviarlo a la función programada en C++.
Y este es el resultado:


 
Ahora, para crear una Dll que reciba y devuelva punteros es necesario hacer unos pequeños cambios:
El archivo se llama "puntero2.cpp" y su código fuente es:


#include <windows.h>
#include "puntero2.h"

BOOL WINAPI DllEntryPoint(HINSTANCE, DWORD, LPVOID)
{
     return TRUE;
}

extern "C"
long* __declspec (dllexport) WINAPI punt (long *puntero, int valor) {

    for (int j=1; j<=valor+1; j++)
        puntero[j]=puntero[j]*2-15;

    return(puntero);

}

"valor" es el tamaño del array cuya dirección está recibiendo la función "punt" la cual, después de ejecutar el bucle, devuelve un puntero. 
Para hacer la Dll también se necesita el archivo "puntero2.h" cuyo código es:

#ifndef _PUNTERO2_H
#define _PUNTERO2_H

#ifdef BUILD_DLL // en la construcción de la librería
#define EXPORT __declspec(dllexport)
#else // en la construcción del ejecutable
#define EXPORT __declspec(dllimport)
#endif

#ifdef __cplusplus /* if in a C++ program */
extern "C"
#endif

long* __declspec (dllexport) WINAPI punt (long *puntero, int valor);

#endif 

Nótese que la función se declara con el asterisco junto al tipo de datos al que apunta el puntero, C++ permite en teoría poner el asterisco al comienzo del nombre de la función, pero al hacer esto el compilador me botaba error, así que es mejor hacerlo de este modo.
"Long" es el tipo de datos para enteros largos (de 64 bits) y lo manejan tanto C++ como Visual Basic. Lo malo es que cuando quise enviar punteros que apuntaban a datos de punto flotante el compilador del Visual Basic me botaba el error 16 en tiempo de ejecución: "Expresión demasiado compleja". Así llegué a descubrir que Visual Basic y Borland C++ sólo pueden intercambiar punteros que apunten a enteros, ya sean Long o Byte. Al trabajar con arrays no recomiendo datos del tipo Integer, al operar me salían resultados erróneos.
Después de compilar y copiar la Dll "puntero2.dll" a la carpeta system32 la probé el Visual Basic con el siguiente código, en un proyecto exe estándar:

Option Explicit

Private Sub Form_Load()
    Dim mat(-1 To 8) As Long
    Dim i As Integer

    For i = 0 To UBound(mat)
        mat(i) = i
    Next

    mat(-1) = punt(mat(-1), UBound(mat))

    For i = 0 To UBound(mat)
        Debug.Print mat(i)
    Next
End Sub

Y colocando en el módulo .bas el siguiente código:

Declare Function punt Lib "puntero2.dll" (ByRef puntero As Long, ByVal valor As Integer) As Long

Esta vez quise ver los valores de "mat" en la ventana del inmediato (ya he fastidiado bastante el Caption del Form) y éste es el resultado:



Ya funciona :)
Ahora la gran pregunta: ¿por qué definí el array "mat" desde -1 y no desde 0? Visual Basic tiene la ventaja de poder definir arrays con índices negativos, cosa que no se puede en C++. El índice -1 terminará conteniendo la dirección de memoria del array, un valor que no interesa a la hora de operar y con el que es mejor no trabajar. El índice -1 en Visual Basic pasará a ser el índice 0 en C++, así el índice 0 para Visual Basic será el índice 1 en C++, etc. Para Visual Basic los índices van de -1 a 8 en este ejemplo, para C++ van de 0 a 9 (la cantidad de elementos del array es 10 en total), donde el valor en el índice 0 no interesa, y no se debe operar con él pues al devolver el puntero al Visual Basic éste "chanca" el primer elemento del array con su dirección de memoria.
Al modificar el bucle del programita en Visual Basic que escribe en la ventana inmediato para que vaya desde -1 se podrá ver el valor de mat(-1):



He aquí a los diez elementos del array
 
Una manera de poder operar con valores de punto flotante es convirtiendo estos valores enteros multiplicándolos por 10^n, donde "n" es la cantidad de decimales con la que vamos a trabajar.
Por ejemplo, queremos una Dll que calcule las raíces cuadradas de algunos números dentro de un vector:
La nueva Dll será "puntero3.dll" y el código fuente de puntero3.h es:


#ifndef _PUNTERO3_H
#define _PUNTERO3_H

#ifdef BUILD_DLL // en la construcción de la librería
#define EXPORT __declspec(dllexport)
#else // en la construcción del ejecutable
#define EXPORT __declspec(dllimport)
#endif

#ifdef __cplusplus /* if in a C++ program */
extern "C"
#endif

long* __declspec (dllexport) WINAPI cuadrado (long *puntero, int valor, long d);

#endif 

Y el de "puntero3.cpp":

#include <windows.h>
#include <math.h>
#include "puntero3.h"

BOOL WINAPI DllEntryPoint(HINSTANCE, DWORD, LPVOID)
{
    return TRUE;
}

extern "C"
long* __declspec (dllexport) WINAPI cuadrado (long *puntero, int valor, long d) {
    double *c;
    c= new double[valor+1];

    for (int j=1; j<=valor+1; j++){
        c[j-1]= double(puntero[j])/d;
        c[j-1]=pow(c[j-1], 0.5);
        puntero[j]= long(c[j-1]*d);
    }
    return(puntero);
}

La variable "d" es el 10^n con el que se trabajará. Aquí hay que tener cuidado de "castear" los arrays de Double a Long y viceversa o sólo nos resultarán ceros.
Y ahora el código fuente del programa en Visual Basic, primero el módulo .bas (todo es una sola línea):

Declare Function cuadrado Lib "puntero3.dll" (ByRef puntero As Long, ByVal valor As Integer, ByVal d As Long) As Long

Y lo que va en el formulario:

Private Sub Form_Load()
    Dim mat() As Double
    Dim matl() As Long
    Dim divisor As Long
    ReDim mat(-1 To 4)
    ReDim matl(-1 To 4)
    divisor = 100000000

    For i = 0 To UBound(mat)
        mat(i) = (i + 1) / 25
        mat(i) = mat(i) * divisor
        matl(i) = CLng(mat(i))
        Debug.Print mat(i) / divisor
    Next

    matl(-1) = cuadrado(matl(-1), UBound(matl), divisor)

    For i = 0 To UBound(mat)
        mat(i) = matl(i) / divisor
        Debug.Print mat(i)
    Next

End Sub

Y el resultado es éste:



Los cinco primeros números son los datos originales, y los otros cinco sus raíces cuadradas hasta ocho decimales de aproximación.
Éste es un mal ejemplo sobre cómo usar una Dll para hacer cálculos numéricos ya que sería mejor sacar las raíces cuadradas directamente con el Visual Basic y usando solamente el array "mat" declarado como Double, pero deja entender cómo se debe manejar los datos para poder operar con números de punto flotante a partir de enteros. Si se trata de números muy grandes y con muchos decimales se pueden poner la parte entera y la decimal en variables separadas, dividir luego en la Dll la parte decimal entre 10^n y sumar ambas variables. Una Dll es útil cuando se requieren cálculos complejos o comandos que el programa principal realizaría más lentamente, con arrays mucho más largos (se declaran los punteros como "far" tanto en el archivo .cpp y el .h) y todas esas cosillas para las que el Visual Basic se vuelve lentecillo (considerando el hardware de inicios de los 2000s)

lunes, 30 de diciembre de 2024

Tutorial MUY viejito: Usando el Puerto Serial Con Visual Basic 6 y Visual C Sharp 2005

Motivo de poner algo tan viejo acá:: lo tenía tirado en mi disco duro, es todo un documento histórico. 

Antes de empezar con este tutorial recomiendo leer: http://www.lammertbies.nl/comm/info/RS-232_null_modem.html porque puede pasar que no se sepa de qué estoy hablando (que es lo más seguro, este tutorial ya tiene veinte años).



Manejo del Puerto Serie: Visual Basic 6


Manejar el puerto serie en VB6 es bastante sencillo con el control MSCOMM. 
Es posible controlar por separado las líneas RTS y DTR por si se las desea manipular independientemente al envío y recepción de datos. Para ello la propiedad de Handshaking debe estar en "none".
En este link: 
https://www.frro.utn.edu.ar/repositorio/catedras/electrica/2_anio/fundamentos_informatica/apuntes/visual_basic/Manejo%20Puertos.pdf (warning: tecnología antigua) da toda la teoría necesaria para tener un control total del puerto serie con VB6, especialmente el manejo de eventos. Los eventos le dicen al MSCOMM si ha habido algún cambio en el puerto; el tipo de evento es recibido por la propiedad CommEvent y según su valor se puede saber qué ha ocurrido (qué pin ha cambiado su estado). Para esto, la evaluación de CommEvent se hace dentro de OnComm del control MSCOMM (en realidad es el único evento que tiene este control).

Para leer caracteres correctamente en una aplicación tipo chat, es mejor dejar la propiedad RThreshold en 1. Así el evento de lectura se disparará cada vez que se reciba un caracter.

Y aquí http://uttinfor.tripod.com/index/id4.html explican las propiedades del MSCOMM.

Aquí se puede descargar mi programa y el código fuente de ejemplo que envía y recibe datos de texto, activa y desactiva las señales RTS y DTR (manipulando las propiedades RTSEnable y DTREnable respectivamente), y evalúa el estado de las líneas CD, DSR y CTS, así como si el puerto puede o no abrirse (en el caso que ya esté abierto por otro programa).

 



Manejo del Puerto Serie: C Sharp 2005


Acá las cosas son un poco más complicadas. Puede optarse por usar el ocx de Visual Basic MSCOMM eligiendo el respectivo componente .com al hacer click sobre el cuadro de herramientas->Elegir Elementos. Pero entonces no tendría sentido hacerlo desde C Sharp en lugar del mismo Visual Basic 6. En este ejemplo he querido usar la plataforma .Net para controlar el puerto serial.

Primero se debe añadir la línea:
using System.IO.Ports; 


Luego, desde en el cuadro de herramientas buscar "serialPort" y jalar este componente a nuestra aplicación (que será del tipo WindowsApplication).

El problema con C Sharp es que la recepción de datos no puede hacerse de manera tan directa como con Visual Basic 6. El evento que detecta que se ha recibido data no puede tratarse igual. 
Debe hacerse tal y como lo muestran en la página 176 de este documento: http://www.slideshare.net/Metaconta/pic-rs232-puerto-serie-con-pic16f84a-presentation 

Yo lo hice exactamente como se indica ahí y me salió :) 


Otro problema que tuve fue el no poder cambiar las propiedades de los controles (como Labels o PictureBox) tan libremente como en VB6. El C Sharp dispara una excepción cada vez que se intenta cambiar las propiedades de los controles en tiempo de ejecución como protección en un entorno multitarea. Para desactivar esta excepción puse en Form1_Load (de la documentación de Microsoft):
CheckForIllegalCrossThreadCalls = false;

Así ya se puede cambiar ciertas cositas en los controles que son necesarias, como se verá más adelante.

Para enviar data del tipo texto, basta la instrucción:
serialPort1.Write(txtEnviar.Text);

Donde txtEnviar es un RichTextBox que contiene el texto a enviar por el puerto.

Para recibir la data se llama a la propiedad: serialPort1.ReadExisting(); que lee todo el buffer de lectura del puerto.

Tuve algunos problemas con el salto de línea a la hora de recibir los datos. Para corregirlos, y hacer que después de enviar el texto salte a la siguiente línea de forma automática en el RichText que recibe los datos, tuve que añadir el caracter 13 (el salto) al texto en txtEnviar y recién enviar la data. Luego restablecía el texto original en txtEnviar sin el caracter 13 al final. Para esto usé la variable data_temp.

La diferencia con Visual Basic, es que C Sharp, a la hora de abrir el puerto, manda las señales CD, DSR y CTS a "true" ó 1. Si se desea usar estas líneas para recibir señales y disparar el evento que detecta cambios en ellas se debe trabajar con la lógica invertida. Es decir que el dispositivo que nos envía esas señales debe usar lógica invertida también.

En cambio si se desea enviar las señales de control RTS y DTR, la lógica de trabajo dependerá de cómo las interprete el dispositivo que las recibe (esto es válido también para VB6). Para este caso, también consideré lógica invertida para las señales RTS y DTR. 1 es desactivado, 0 es activado.
Así, en el control serialPort se colocan sus propiedades RtsEnable y DtrEnable como "true" para que tengan estos valores al iniciar la aplicación. Si se desea activar, su estado pasará a "false".

Para controlar los eventos, o los cambios en las líneas de entrada del puerto serial, me basé en esté código:

Imports System.IO.Ports
Public Class Form1
    Private WithEvents mPort As SerialPort

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        mPort = New SerialPort("COM1", 9600)
        mPort.Open()
    End Sub

Private Sub mPort_PinChanged(ByVal sender As Object, ByVal e As System.IO.Ports.SerialPinChangedEventArgs) Handles mPort.PinChanged
        If e.EventType = SerialPinChange.CDChanged Then
            If mPort.CDHolding Then
                Debug.Print("Carrier detect on")
            Else
                Debug.Print("Carrier detect off")
            End If
        End If
    End Sub
End Class

Fuente: http://social.msdn.microsoft.com/Forums/en-US/netfxbcl/thread/ffbb63a1-5a04-46c9-a71a-f52b6f7ada81/ (el enlace ya no existe)


Está hecho para Visual Basic 2005 así que tuve que "traducirlo" a C Sharp y adaptarlo para que reconociera los eventos que me interesaban. La lógica es la misma que con VB6: con un "switch" evaluar qué evento ha ocurrido según el valor de la propiedad que los recibe. En C Sharp es e.EventType y se evalúa versus las propiedades de serialPort CDChanged, CtsChanged y DsrChanged. Estas propiedades sólo saben si hubo un cambio en las señales CD, CTS y DSR, no conocen sus estados (para saber los estados se usan otras propiedades que se explican más abajo).

Ambos programas, el de VB6 y C Sharp funcionan igual y hacen lo mismo. La única diferencia es que el de C Sharp emplea lógica invertida. 

El código fuente y el programa en C Sharp se puede bajar aquí.

Si los "leds" que indican el estado de las señales CD, DSR y CTS en VB6 son controles tipo "Shape", en C Sharp usé controles Picturebox. Dibujar un círculo y un cuadrado en C Sharp es muy engorroso, llegué a crear una clase para ello, pero cuando quería dibujar dos círculos, al final el programa me los dibujaba uno encima de otro sin importar lo que hiciera para evitarlo. Creé dos propiedades para pasarle el valor de las coordenadas, pero igual no funcionó. Haciendo puntos de quiebre descubrí que el C Sharp primero asigna los valores a las variables con los valores de las coordenadas y luego dibuja todo lo que se le pide, usando sólo los valores finales de estas variables. Después de mucho tratar decidí no dibujar nada. Para quienes quieran intentar hallarle una solución, les dejo la clase:

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;

namespace WindowsApplication1
{
    /* en el programa principal crear un objeto.
    Shapes cuad = new Shapes();
    Estado.Paint += new PaintEventHandler(cuad.Shapes_Fill);
    Estado.Paint += new PaintEventHandler(cuad.Shapes_Paint); 
    */


    class Shapes
    {
        private Graphics g;
        private int _coorX;
        private int _coorY;

        public Shapes()
        {
            _coorX = 30; 
            _coorY = 30; //sólo para inicializarlas
        }

        public int coorX
        {
            get { return _coorX; }
            set { _coorX = value; }
        }

        public int coorY
        {
            get { return _coorY; }
            set { _coorY = value; }
        }

        public void Shapes_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
        
            g = e.Graphics;
            Pen p = new Pen(Color.Black,2) ;
            g.DrawEllipse(p, 30, 30 , 30, 30); 
        }

        public void Shapes_Fill(object sender, System.Windows.Forms.PaintEventArgs e) 
        
            g = e.Graphics;
            Pen p = new Pen(Color.Black, 2);
            //g.DrawEllipse(p, _coorX , _coorY, 30, 30); 
            g.FillEllipse(Brushes.Firebrick, 30, 80, 30, 30); 
        }
    }
}


Al final preferí usar tres picturebox para los "leds" de estado de CD, DRS y CTS, así simplemente cambiaba su color según el estado de estas líneas (para poder hacer esto tuve que poner a "false" la propiedad CheckForIllegalCrossThreadCalls mencionada más arriba). Estos estados se obtienen evaluando las propiedades de serialPort CDHolding, DsrHolding y CtsHolding respectivamente (estas propiedades son las mismas que en VB6).

Otra diferencia que hallé es que si se deja la línea CD sin conexión, VB6 no le hace nada, pero C Sharp la manda a cero al enviar data. Como está sin conectar no hay forma de ponerla a uno nuevamente.

Ambos programas los probé con el test de Loop Back puenteando los puntos 7-8-1, 4-6 y 2-3. Cualquiera de los dos ejecutables pueden usarse como programas independientes para probar el puerto serie. En el caso del C Sharp, el ejecutable está en \WindowsApplication1\

martes, 25 de junio de 2024

Tutorial viejito: Creando una dll en Borland C++ para luego usarla en Visual Basic 6

 Otro tutorial de los viejitos, de hace casi 20 años! Enjoy!

Visual Basic es un lenguaje de programación con claras ventajas: Es fácil y rápido de programar, cuenta con abundante documentación, recursos y herramientas gratuitos botados por toda la Internet, permite redimensionar arrays, jalar datos desde archivos en Office, se lleva bien con las Apis, etc, etc. 

Pero también tiene sus desventajas: sus aplicaciones tienden a ser un tanto más lentas y pesadas que las programadas en otros lenguajes, depende de librerías que no todo el mundo tiene, es un compilador propietario de Microsoft, no maneja sentencias en asm, no permite declarar variables binarias tipo word o byte y manipularlas bit por bit, etc, etc.  

Es decir: todas esas cosas que tiene el lenguaje C++ pero que le faltan al Visual Basic. No digo que uno sea mejor que el otro, sino que uno tiene lo que al otro le falta. Si tuviera que elegir, haría las GUIs de mis programas en Visual, pero la parte del manejo de arrays, bucles, cálculo matemático, lectura/escritura en el puerto paralelo, entre otras cosas más, se lo dejaría al C++. Al Visual Basic le dejaría las Apis y el puerto serial, además de intercambiar datos con archivos de Office o leer imágenes desde el disco duro. 

En realidad, existe una manera de combinar las bondades de ambos lenguajes: hacer las GUIs del programa en Visual Basic y ordenarle que jale las funciones que necesita desde Dlls hechas y compiladas en C++. El uso de Dlls tiene claras ventajas: si todo el código está metido en un exe éste será más pesado y, a la hora de ejecutarlo, se cargará todo en la memoria RAM. Pero si hace llamadas a Dlls, éstas sólo se cargarán en la RAM en el momento de ser usadas, lo cual liberará espacio en memoria y hará que el programa corra más rápido.  

1. Cómo hacer la Dll:

Primero hay que bajar el compilador gratuito que ofrece Borland.  Incluye un ide, pero este tutorial está hecho para la versión original sin ide. La carpeta Borland se debe copiar a la partición C:, por lo que la ruta del ejecutable que realiza la compilación (el bcc32.exe) queda en C:\Borland\BCC55\Bin\.

Luego se debe ir a la carpeta C:\Borland\BCC55\Lib\ y copiar todos los archivos (excepto la carpeta PSDK) a la carpeta Bin, donde está bcc32.exe. Esto es porque a la hora de compilar, el compilador busca los archivos obj de la carpeta Lib, si no los copiamos a la misma carpeta donde está bcc32.exe nos botará error. 

En la misma carpeta Bin, desde el block de notas, se debe crear el archivo que será nuestra Dll, debe tener extensión cpp y el nombre que llevará la dll. Yo la llamaré inicio.cpp. El código es:

// inicio.cpp

#include <windows.h>
#include "inicio.h"


BOOL WINAPI DllEntryPoint(HINSTANCE, DWORD, LPVOID)
{
return TRUE;
}

extern "C"
int __declspec (dllexport) WINAPI suma (int a, int b) {
return a+ b;
}

La primera función WINAPI es "el puntero de entrada" que necesita Visual Basic para poder usar la Dll. Borland C++ permite declarar esta función como "BOOL WINAPI dllMain(HINSTANCE, DWORD, LPVOID)", pero en este caso Visual Basic generará error al no hallar el puntero de entrada. (Hay que notar que no existe una función main o winMain en la dll)

De la forma que sea, esta función DllEntryPoint deberá retornar siempre el valor de TRUE o se generará error.  

La siguiente función es la que programé y hace una simple sumatoria de dos enteros. La sintaxis en general es:

extern "C"
tipo __declspec (dllexport) WINAPI nombre_funcion (argumentos) { ... }

Hay que observar que antes de "declspec" pueden ir uno o dos guiones largos. 

Se debe notar que el archivo "inicio.cpp" también llama a un archivo "inicio.h", para crear la dll ambos son necesarios. El código de "inicio.h" es:

#ifndef _INICIO_H
#define _INICIO_H

#ifdef BUILD_DLL // en la construcción de la librería
#define EXPORT __declspec(dllexport)
#else // en la construcción del ejecutable
#define EXPORT __declspec(dllimport)
#endif

#ifdef __cplusplus /* if in a C++ program */
extern "C"
#endif

int __declspec(dllexport) WINAPI suma(int a, int b);

#endif 

Lo importante aquí está en la penúltima línea de código: la declaración de la función que se "exporta" desde la dll y que devolverá los valores al programa principal. 

La Compilación:

Ambos archivos, "inicio.cpp" e "inicio.h" deberán estar en la misma carpeta donde está el ejecutable compilador, la carpeta deberá tener estos archivos:


Luego se va a Inicio->Ejecutar->cmd (para windows 2000 hacia arriba, para windows 98 es Inicio->Ejecutar->Command), nos situamos en la carpeta del compilador y se ingresa el siguiente comando (va todo seguido):


El compilador necesita varios parámetros para hacer lo que queremos, ahora los voy a explicar:

"-einicio": todo lo que va después de "-e" y sin espacios, será el nombre de la dll.

-Q: Es para que nos dé la información de la compilación y si se generó algún error.

-I: este comando, seguido de una ruta o varias rutas separadas por punto y coma sin espacios, le indica al compilador dónde buscar las cabeceras que se están usando. Como "windows.h" está en la carpeta Include, debemos decirle que busque allí o botará error. 

-WD: es el comando para generar una dll y no un exe. 

Después de todo esto, le decimos qué archivo se quiere compilar.

Para más información, recomiendo revisar el archivo de ayuda que viene con el compilador y que está en la carpeta Help.

Si todo sale bien, deberá aparecer:


Después de esto, la dll deberá aparecer en la carpeta donde tenemos los archivos "inicio¡.cpp" e "inicio.h" con el nombre de "inicio.dll". Esta dll ya podemos copiarla a donde la necesitemos para poder usarla (se recomienda ponerla en la carpeta system32).

 

Probando la dll:

Abrimos el visual con un proyecto Exe Estándar. Le añadimos un módulo .bas nuevo y le ponemos este código:

Public Declare Function suma Lib "inicio.dll" (ByVal a As Integer, ByVal b As Integer) As Integer

Dentro de la Dll está la función "suma" (pueden ser varias funciones, entonces tenemos que jalarlas una por una), con los dos argumentos tipo Entero. La función devuelve un entero, por ello se la declara "As Integer". Si la función en C++ fuera del tipo "void" no se pone al final el tipo de dato pues no devuelve ningún valor (incluso en algunas webs vi que las funciones tipo "void" de C++ eran llamadas como "Sub" en lugar de "Function" desde Visual Basic, pero esto no lo he probado).

En el formulario, el único código que puse para probar si devolvía un valor correcto fue:

Private Sub Form_Load()
    Dim c As Integer
    c = suma(8, 4)
    Form1.Caption = c
End Sub

Y lo que salió fue:

Ya funciona :)

Y ahora, una web donde explican un poco más esto de las Dlls generadas con el Borland C++:

http://www.zator.com/Cpp/E1_4_4b2a.htm

miércoles, 18 de agosto de 2021

Modificando los Atributos de los Archivos con Visual Basic 6

Otra entrada migrada de la vieja web Electrónica a Martillazos, porque desde hace una década no hago más que programar y programar, asi que de electrónica ya nada. Y de paso me deshago de la fuente Comic Sans :D


De vez en cuando los virus infectan las computadoras de la facultad. Hubo uno muy molesto que modificaba los atributos de los archivos de tal forma que no pudieran "desocultarse":

Desde la ventana de "propiedades" es imposible "desocultar" el archivo. Intenté hallar cómo modificar esta propiedad, rebusqué hasta en el registro y nada.

Hasta que me topé con esta web: http://www.recursosvisualbasic.com.ar/htm/tutoriales/tutorial-basico7.htm#getattr-setattr

Dado que desde Visual Basic es muy pero muy fácil modificar los atributos de los archivos decidí hacerme mi propio programa para quitar las modificaciones que hizo el condenado virus:

Se abre el Visual y se carga un proyecto exe estándar. Luego, a la barra de controles, se le carga el control Conmon Dialog. Cómo hacerlo está acá: http://www.recursosvisualbasic.com.ar/htm/tutoriales/control-commondialog.htm

Éste control servirá para elegir el archivo cuyos atributos se van a modificar, ésta es la ventana que creé:

 


Ésta es la lista de los controles:

- Un Label para ver la ruta del archivo: label1

- Un Conmon Dialog: cmd

- Dos botones, uno para llamar al Conmon Dialog y escoger el archivo, y el otro para modificar sus atributos: Command2 y Command1 respectivamente.

- Seis OptionButton para escoger los atributos que se deseen modificar, sus nombres, de arriba a abajo y muy explícitos, son:

normal, lectura, oculto, sistema, s_oculto, s_oculto_lectura.

Cada OptionButton asigna un número a una variable tipo Integer llamada "attr" ya que los atributos están definidos por números, acá la lista de todos los que pude encontrar:

0 - Archivo Normal

1 - Archivo de Sólo Lectura

2 - Archivo Oculto

4 - Archivo de Sistema

16 - Directorio o Carpeta

32 - Archivo Modificado

38 - Archivo de Sistema Oculto

39 - Archivo de Sistema Oculto y Sólo Lectura

18 - Carpeta Oculta

6 - Oculto (en la caja ventana de propiedades no está el check en "archivo")

7 - Archivo? de sistema oculto y sólo lectura (en la caja ventana de propiedades no está el check en "archivo", es posible que sea aplicable para carpetas, no lo he comprobado)

Y el código fuente es (un par de líneas comentadas se colaron por ahí):

Option Explicit

Dim ruta As String
Dim ret As Long
Dim attr As Integer

Private Sub Command1_Click()
'cmd.Action = 1

If ruta <> "" Then
SetAttr ruta, attr
End If

End Sub

Private Sub Command2_Click()

cmd.DialogTitle = "Seleccione un archivo"
cmd.ShowOpen

If cmd.FileName <> "" Then
ruta = cmd.FileName
Label1 = ruta
End If

End Sub

Private Sub Form_Load()

'ret = GetAttr("c:\IO.SYS")
Me.Top = Screen.Height / 2 - Me.Height / 2
Me.Left = Screen.Width / 2 - Me.Width / 2
attr = 0

End Sub

Private Sub lectura_Click()
attr = 1
End Sub

Private Sub normal_Click()
attr = 0
End Sub

Private Sub oculto_Click()
attr = 2
End Sub

Private Sub s_oculto_Click()
attr = 38
End Sub

Private Sub s_oculto_lectura_Click()
attr = 39
End Sub

Private Sub sistema_Click()
attr = 4
End Sub

 

Y para quienes no quieren programarse esto, les dejo el ejecutable que me llevó 15 minutos de mi vida hacerlo.

martes, 16 de julio de 2019

(Tutotial viejito): Creando Un Ocx Para Reproducir Video Con La Api Mcisendstring

Este es otro tutorial migrado de la vieja web:

Sucedió que necesité mucho mucho un ocx para reproducir video, pero no cualquier ocx: no debía depender de componentes instalados con anterioridad, lo que yo quería era un ocx que trabajara directamente con las Apis del windows.
Las Apis son funciones del propio Windows, guardadas dentro de los millones de Dlls en la carpeta de instalación (especialmente en la System32). Las Apis son casi un millar y lo hacen todo: obtienen información de los discos y las particiones, las características del escritorio, la pantalla, los colores, el hardware, los archivos, manejan multimedia, los periféricos, etc etc.

Rebuscando en internet, hallé muchos códigos fuentes que llamaban a las Apis que manejan multimedia: MciExecute, MciSendCommand y MciSendString.

De todas, la mejor es la MciSendString. La primera tiene opciones un tanto limitadas, la segunda requiere que se le pasen parámetros en hexadecimal, mientras que la última permite cadenas fácilmente entendibles.


Cómo funciona MciSendString:

Esta Api devuelve un valor tipo Long que es un código por si ocurre un error. Para saber qué error ha sido se llama a otra Api: mciGetErrorString, la cual no usé para este ocx.


Y fue gracias a estos ejemplos que aprendí a usar la MciSendString:



Lo primero que se debe hacer con esta Api es enviarle la ruta del archivo multimedia que se quiere reproducir:

"vid" es una variable tipo Long, "rutavideo" es una variable tipo String y contiene la ruta del archivo, algo así como:

C:\carpeta\ese_archivo.mpg 


 Lo que primero se hace es ponerle las comillas al principio y al final.


rutavideo = Chr$(34) & rutavideo & Chr$(34) 'para agregarle las comillas
vid = mciSendString("open " & rutavideo & " type mpegvideo alias movie parent " & Picture1.hWnd & " style " & "child ", 0, 0, 0)
vid = mciSendString("put movie window at 0 0 336 240", 0&, 0, 0)

La segunda y tercera líneas le dice que abra el archivo de esa ruta como un video del tipo mpeg (todos los videos que quería reproducir son mpegs) y que lo reconozca como "movie" porque así es más fácil de escribir (¡observad el espacio después de "open"! aquí hay que respetar los espacios!!). 
En esta segunda línea también se puede poner simplemente:



vid = mciSendString("open "& rutavideo & type mpegvideo alias movie", 0&, 0, 0)
 


Pero esto hará que cuando se reproduzca el video
lo haga en una ventana Popup, y yo quería reproducirlo dentro de mi control ocx,
así que por ello le incluí un Picturebox. La propiedad "Picture1.hWnd"
devuelve un número tipo Long que es el identificador del control donde se desea
reproducir el video (puede ser un form también), así se evita que aparezca esa
ventana Popup. 
El resto de las instrucciones es más simple (basta recordar que ya la Api reconoce a nuestro archivo y su ruta como "movie"):

vid = mciSendString("play movie", 0, 0, 0)
vid = mciSendString("pause movie", 0, 0, 0)
vid = mciSendString("resume movie", 0, 0, 0)
vid = mciSendString("stop movie", 0, 0, 0)


Para reproducir desde un tiempo determinado ("tiempo" es una variable tipo Long):
vid = mciSendString("play movie from " & tiempo, 0, 0, 0)

Una muy importante es:
vid = mciSendString("close movie", 0, 0, 0)


Si no se "cierra" nuestro archivo, a la siguiente que se quiera reproducir no aparecerá. Esta instrucción debe estar en el evento Unload del form y cada vez que se quiere reproducir otro video.

Hasta aquí lo básico para hacer un reproductor con MciSendString. Ahora falta meterlo todo en un ocx, al cual llamé "video_rep".
Cómo crear un ocx está en esta web:
http://www.elguruprogramador.com.ar/articulos/creando-un-control-activex-ocx.htm
Lo que hice fué combinar ambas cosas: los comandos del MciSendString dentro de sus respectivas funciones en el ocx para luego "jalarlas" desde una aplicación.
El código fuente de video_rep y unejemplo de cómo usarlo está akí.
Este ocx aún puede mejorarse y adaptarse. Quise hacerlo de modo que pudiera usarse con o sin Slider, bueno, yo sólo lo he probado con Slider.

Y ahora, una excelente web con recopilación de trillones de Apis:


http://allapi.mentalis.org/index2.shtml

Recomiendo mucho bajar el ApiViewer y el Api-Guide

viernes, 12 de septiembre de 2014

Cómo ejecutar Visual Studio 6 en Windows 7 y evitar los errores con referencias y dependencias

Tengo un Visual Basic 6 Enterprise corriendo en Windows 7 de 32 bits. Todo parece funcionar bien: se ha instalado sin problemas, y se ejecuta sin inconveientes.
Pero al abrir un proyecto nuevo del tipo Exe estándar, e intentar cargar librerías y ocx de terceros (no son de Microsoft, o no están firmadas por Microsoft) me salta este error "Error al intentar tener acceso al registro del sistema":


(No lo parece, pero esto es un Windows 7, sólo que con el tema de Windows Clásico. Los colorcitos sólo sirven para consumir memoria, otra razón es que así puedo aluscinar que tengo un Windows Server)


Este fue un error muy feo que me tuvo ocupada durante horas (además de hacerme instalar y desinstalar las ocx varias veces), y al final la solución era muy simple, ejecutar el Visual Studio 6 como Administrador:


Ya con todos los permisos disponibles, el Windows nos deja importar componentes, referencias y dependencias. Y como se muestra en la imagen, el Visual Studio 6 puede convivir perfectamente con el Visual Studio 2013 (yo tengo la versión Express).
Un último consejo: si se tiene un proyecto de Visual Basic 6 con dependencias de terceros, es mejor ejecutar primero el IDE como administrador, y luego abrir nuestro proyecto. Se evitarán así muchos errores.

miércoles, 23 de octubre de 2013

Cómo evadir la limitación de tamaño de una ventana en Windows

Por diseño, Microsoft Windows limita el tamaño de las ventanas a (en píxeles):
(12 + ancho de la resolución de pantalla) x (12 + alto de la resolución de pantalla)

Más información en StackOverflow.

Por allí hallé quienes afirman que se puede superar esta restricción. Y entonces me topé con este código, de la misma Microsoft, en Visual Basic 6.

Lo copio aquí para que no se vaya a perder:

En un módulo bas:

 Option Explicit

      Private Const GWL_WNDPROC = -4
      Private Const WM_GETMINMAXINFO = &H24

      Private Type POINTAPI
          x As Long
          y As Long
      End Type

      Private Type MINMAXINFO
          ptReserved As POINTAPI
          ptMaxSize As POINTAPI
          ptMaxPosition As POINTAPI
          ptMinTrackSize As POINTAPI
          ptMaxTrackSize As POINTAPI
      End Type

      Global lpPrevWndProc As Long
      Global gHW As Long

      Private Declare Function DefWindowProc Lib "user32" Alias _
         "DefWindowProcA" (ByVal hwnd As Long, ByVal wMsg As Long, _
          ByVal wParam As Long, ByVal lParam As Long) As Long
      Private Declare Function CallWindowProc Lib "user32" Alias _
         "CallWindowProcA" (ByVal lpPrevWndFunc As Long, _
          ByVal hwnd As Long, ByVal Msg As Long, _
          ByVal wParam As Long, ByVal lParam As Long) As Long
      Private Declare Function SetWindowLong Lib "user32" Alias _
         "SetWindowLongA" (ByVal hwnd As Long, _
          ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
      Private Declare Sub CopyMemoryToMinMaxInfo Lib "KERNEL32" Alias _
         "RtlMoveMemory" (hpvDest As MINMAXINFO, ByVal hpvSource As Long, _
          ByVal cbCopy As Long)
      Private Declare Sub CopyMemoryFromMinMaxInfo Lib "KERNEL32" Alias _
         "RtlMoveMemory" (ByVal hpvDest As Long, hpvSource As MINMAXINFO, _
          ByVal cbCopy As Long)

      Public Sub Hook()
          'Start subclassing.
          lpPrevWndProc = SetWindowLong(gHW, GWL_WNDPROC, _
             AddressOf WindowProc)
      End Sub

      Public Sub Unhook()
          Dim temp As Long

          'Cease subclassing.
          temp = SetWindowLong(gHW, GWL_WNDPROC, lpPrevWndProc)
      End Sub

      Function WindowProc(ByVal hw As Long, ByVal uMsg As Long, _
         ByVal wParam As Long, ByVal lParam As Long) As Long
          Dim MinMax As MINMAXINFO

          'Check for request for min/max window sizes.
          If uMsg = WM_GETMINMAXINFO Then
              'Retrieve default MinMax settings
              CopyMemoryToMinMaxInfo MinMax, lParam, Len(MinMax)

              'Specify new minimum size for window.
              MinMax.ptMinTrackSize.x = 200
              MinMax.ptMinTrackSize.y = 200

              'Specify new maximum size for window.
              MinMax.ptMaxTrackSize.x = 500
              MinMax.ptMaxTrackSize.y = 500

              'Copy local structure back.
              CopyMemoryFromMinMaxInfo lParam, MinMax, Len(MinMax)

              WindowProc = DefWindowProc(hw, uMsg, wParam, lParam)
          Else
              WindowProc = CallWindowProc(lpPrevWndProc, hw, uMsg, _
                 wParam, lParam)
          End If
      End Function


El truco es fácil: donde están los números en rojo, escribimos nuestros números enormes favoritos. Yo le puse por código a una ventana (Form) dimensiones de 2050*1600 píxeles, en una resolución de pantalla de 1024*768 (la restricción por defecto sería 1036*780).

Para hacer esto en el form ponemos:

      Option Explicit

      Private Sub Form_Load()
          'Save handle to the form.
          gHW = Me.hwnd

          'Begin subclassing.
          Hook 
          ' le ponemos un tamaño ENORME 
          Me.Height = 1600
          Me.Width = 2050
      End Sub

      Private Sub Form_Unload(Cancel As Integer)
          'Stop subclassing.
          Unhook
      End Sub 


Y funcionó.

Ya no intenté con dimensiones mayores porque una ventana con aproximadamente el doble de alto y ancho que la resolución de pantalla parece ser suficientemente grande. Deduzco que el máximo que puede aguantar el sistema operativo depende de la memoria máxima que le puede asignar a semejante ventanón y antes que la RAM empiece a echar humo.

Como son puras llamadas a Apis de WIndows, se puede traducir este código a Visual C++ o a .Net usando DLLImport.