Remote View
Fuentes: source/internal/remoteview/remoteviewfw.c, server.c, capture.c
Remote View permite que cualquier navegador de la LAN vea y controle en directo
una ventana FWH activa. La ventana se captura en tiempo real con GDI+, se
transmite como fotogramas JPEG a través de un WebSocket y se dibuja en un
canvas HTML5. Los clics del ratón, el movimiento, la rueda y la entrada de
teclado regresan desde el navegador a la ventana real mediante
PostMessage.
Inicio Rápido
Inicie un servidor en ON INIT y deténgalo en VALID.
Apunte cualquier navegador de la LAN a http://<ip>:8080:
#include "FiveWin.ch"
function Main()
local oWnd, oSay, oBtn
local nClicks := 0
DEFINE WINDOW oWnd TITLE "FWH Remote View" ;
FROM 4, 4 TO 22, 60
@ 1, 2 SAY oSay PROMPT "Open http://localhost:8080" ;
OF oWnd SIZE 300, 20
@ 3, 2 BUTTON oBtn PROMPT "Click (local or remote)" ;
OF oWnd SIZE 160, 30 ;
ACTION ( nClicks++, oSay:SetText( "Clicks: " + Str( nClicks, 4 ) ) )
ACTIVATE WINDOW oWnd ;
ON INIT ( RemoteViewStart( oWnd:hWnd, 8080 ), ;
oWnd:SetText( "FWH Remote View - http://localhost:8080" ) ) ;
VALID ( RemoteViewStop(), .T. )
return nil
El navegador muestra la ventana tal cual — botones, SAYs, GETs, browses, menús, todo funciona. Los clics en el canvas se convierten en clics reales sobre la ventana real.
Arquitectura
| Componente | Archivo | Función |
|---|---|---|
| Enlace con Harbour | remoteviewfw.c |
Expone RemoteViewStart/Stop/Serve/StopAll/SetOffset al PRG |
| Servidor WebSocket | server.c |
Servidor Winsock de un solo hilo y sin bloqueo, HTML/JS del cliente, protocolo binario |
| Motor de captura | capture.c |
Captura de pantalla con GDI+, codificación JPEG diferencial (región cambiada), modo viewport |
| Handshake WebSocket | sha1.c, base64.c |
Hash SHA-1 y codificación Base64 para el handshake de actualización WebSocket |
Referencia de la API de Harbour
RemoteViewStart( [hWnd], [nPort] ) → lOk
Inicia la captura de hWnd (por defecto: la ventana activa) y la
sirve en nPort (por defecto 8080). Devuelve .T. si
tiene éxito.
Estrategia de bombeo: Una ventana oculta de solo mensajes posee
un WM_TIMER (16 ms) que llama a ServerTick(). Dado que
el temporizador lo despacha el bucle de mensajes del hilo, sigue disparándose
incluso mientras un bucle modal (MsgInfo, diálogos, menús) ha tomado el
control del bucle — justo lo que FWH necesita. Sin hilos, sin
-mt requerido.
RemoteViewStop()
Detiene el servidor, elimina el temporizador de bombeo, libera GDI+ y limpia el
estado. Llámelo en VALID o antes del cierre.
RemoteViewSetOffset( nX, nY )
Desplaza la imagen de la ventana dentro del viewport del navegador
nX píxeles a la derecha y nY píxeles hacia abajo.
Por defecto 0,0. Útil durante el desarrollo para que la ventana real no
cubra la imagen web en el mismo monitor. Llámelo antes de
RemoteViewStart().
RemoteViewServe( hWnd, nPort [, nOffX, nOffY] )
Bloqueante. Inicia un servidor con el estado aislado mediante TLS
(almacenamiento local de hilo) y bombea ServerTick() en un bucle
hasta que se llama a RemoteViewStopAll(). Diseñado para
ejecutarse dentro de un hilo de Harbour creado con
hb_threadStart(), de modo que cada callback PRG (ACTION,
bChanged…) se ejecute con la VM de Harbour ya inicializada en ese hilo.
Requiere -mt (VM MT de Harbour).
RemoteViewStopAll()
Indica a todos los bucles RemoteViewServe() en ejecución que
finalicen. Llámelo en VALID cuando use el enfoque multicliente MT.
Dos Modos
Modo Cliente Único (bombeo WM_TIMER)
Use RemoteViewStart() en ON INIT. Un servidor, un
cliente. No se necesita -mt. El bombeo del temporizador se ejecuta
en el hilo de la GUI y sobrevive a los bucles modales. Ideal para la mayoría de
las aplicaciones.
| Ejemplo | Descripción |
|---|---|
samples/RemoteView/testrv.prg |
Ventana básica con SAY + BUTTON, un cliente en el puerto 8080 |
samples/RemoteView/testrv2.prg |
Ventana principal + diálogo hijo. La captura compone los popups propios, por lo que el diálogo también aparece en el navegador |
samples/RemoteView/testrvweb.prg |
Remote View + TWebView2 (Edge HTML/JS dentro de la ventana publicada) |
samples/database/fivedburv.prg |
FiveDBU integrado con Remote View: ON INIT RemoteViewStart() + menú emergente con clic derecho |
Modo Multicliente (hilos de Harbour + -mt)
Use hb_threadStart() para lanzar N instancias de
RemoteViewServe(), cada una en su propio puerto. El aislamiento de
estado por TLS evita que las instancias se pisen entre sí. Varios navegadores
pueden ver y controlar la misma ventana simultáneamente, cada uno en un puerto
distinto. Deténgalos con RemoteViewStopAll().
#define N_CLIENTS 3
#define BASE_PORT 8080
STATIC FUNCTION StartServers( oWnd )
local i
for i := 0 TO N_CLIENTS - 1
hb_threadStart( {| h, p | RemoteViewServe( h, p, 200, 200 ) }, ;
oWnd:hWnd, BASE_PORT + i )
next
oWnd:SetText( "FWH Remote View MT - " + LTrim( Str( N_CLIENTS ) ) + ;
" servers (8080.." + LTrim( Str( BASE_PORT + N_CLIENTS - 1 ) ) + ")" )
return nil
Ejemplo: samples/RemoteView/testrvmt.prg.
Compílelo con build_new.bat testrvmt mt
(mt como 3er argumento selecciona la VM MT de Harbour).
Protocolo Cliente-Navegador
El servidor sirve un cliente HTML/JS en línea en GET /. El cliente
integrado abre un WebSocket hacia /ws y usa un protocolo binario
compacto:
| Byte 0 | Mensaje | Carga útil |
|---|---|---|
0x01 | Fotograma completo | w(2) h(2) originX(2) originY(2) JPEG... |
0x02 | Parche sucio | x(2) y(2) w(2) h(2) originX(2) originY(2) JPEG... |
0x10 | Movimiento de ratón | x(2) y(2) |
0x11 | Botón de ratón pulsado | x(2) y(2) button(1) |
0x12 | Botón de ratón liberado | x(2) y(2) button(1) |
0x13 | Tecla pulsada | vk(2) modifiers(1) char(2) |
0x14 | Tecla liberada | vk(2) modifiers(1) |
0x15 | Rueda del ratón | x(2) y(2) delta(2) |
0x16 | Tamaño del viewport | w(2) h(2) (cliente → servidor) |
Todos los enteros multibyte son little-endian. Las coordenadas están en el espacio del bitmap (el servidor las asigna a coordenadas de pantalla antes de PostMessage).
Captura Inteligente
El motor de captura usa varias técnicas para minimizar el ancho de banda:
- Captura diferencial: Solo la región cambiada de la
ventana se codifica en JPEG y se envía como un parche
0x02. El navegador lo dibuja en el desplazamiento correcto, conservando el resto del canvas. - Hash FNV-1a: Cada fotograma se somete a hash. Los fotogramas consecutivos idénticos se omiten por completo — sin codificación JPEG, sin envío por la red.
- Modo viewport: Cuando el navegador envía el tamaño de su
viewport (
0x16), el motor de captura escala el escritorio virtual para que coincida con el canvas del navegador. La ventana se centra dentro de este espacio virtual. Maximizar la ventana llena el viewport del navegador, no el monitor real. - Minimizar: Cuando la ventana está minimizada, se dibuja una representación icónica que se puede pulsar en el navegador.
- Popups propios: Los diálogos y MessageBoxes propiedad de la ventana capturada se componen encima, de modo que aparecen en el navegador.
Interacción con la Barra de Título
El cliente del navegador admite la interacción completa con la barra de título:
- Arrastrar para mover: Haga clic y arrastre el área de la barra de título para mover la ventana
- Bordes de redimensionado: Bordes de 8 píxeles en todos los lados y esquinas para redimensionar
- Doble clic en la barra de título: Alterna entre maximizar y restaurar
- Botones Min/Max/Cerrar: Se pueden pulsar con realimentación visual al pasar el cursor
- Compatible con DWM: Se ajusta a los límites extendidos del marco DWM de los Windows 10/11 modernos, de modo que la detección de pulsación de los botones funciona correctamente con las esquinas redondeadas y las sombras
Soporte de Teclado
El cliente del navegador captura los eventos de teclado y los asigna a los códigos de tecla virtual de Windows. Se admiten las teclas estándar (Enter, Escape, Retroceso, Tab, flechas, teclas de función, alfanuméricas). Las teclas modificadoras (Shift, Ctrl, Alt, Meta, BloqMayús) se rastrean y se envían con cada evento de tecla.
Nota de Seguridad
Advertencia: El servidor se enlaza a INADDR_ANY
(0.0.0.0) sin autenticación. Cualquier dispositivo de la LAN puede ver y
controlar la ventana. Úselo solo en redes de confianza hasta
que se añada una opción de solo local o de autenticación.
Compilación
Remote View está enlazado en todas las bibliotecas FWH — no se necesitan libs
adicionales. El servidor WebSocket usa Winsock (ws2_32.lib) y el
motor de captura usa GDI+ (gdiplus.lib). build_new.bat
enlaza ambos automáticamente para todas las variantes de compilador.
El RemoteView.exe interno en
source/internal/remoteview/ es una implementación de referencia en
C puro que no requiere FiveWin — una ventana Win32 mínima servida por el mismo
servidor y motor de captura.
- Cliente único:
build_new.bat testrv(cualquier variante) - Multicliente:
build_new.bat testrvmt mt(se requiere la VM MT de Harbour)
Compatibilidad con Compiladores
Se verificó que el Remote View de cliente único captura y transmite
correctamente en todas las variantes de Harbour y xHarbour, de 32 y 64
bits, en los compiladores C BCC, MSVC y MinGW. El título de la ventana
testrv / testrvmt informa de la compilación en
ejecución en tiempo de ejecución (por ejemplo “Harbour MSVC 64”),
lo que facilita confirmar qué cadena de herramientas produjo un ejecutable dado.
El modo multicliente (-mt) se basa en la VM multihilo de Harbour y
su API C de hilos, por lo que está disponible en las compilaciones Harbour
BCC32 / MSVC32 / MSVC64; xHarbour no proporciona esa API.
xHarbour Commercial (VC98) es la única excepción: las
aplicaciones FiveWin ordinarias se compilan y ejecutan allí, pero la captura de
ventanas con GDI+ de la que depende Remote View (la misma ruta usada por
SaveAsImage) funciona mal en tiempo de ejecución, por lo que Remote
View no es compatible con esa compilación.
Patrón de Integración
El patrón recomendado para añadir Remote View a cualquier aplicación FWH:
ACTIVATE WINDOW oWnd ;
ON INIT ( RemoteViewSetOffset( 200, 200 ), ; // optional dev offset
If( RemoteViewStart( oWnd:hWnd, 8080 ), ;
oWnd:SetText( "App - http://localhost:8080" ), ;
MsgStop( "Could not start Remote View" ) ) ) ;
VALID ( RemoteViewStop(), .T. )
Para aplicaciones que necesitan varios clientes simultáneos, use
hb_threadStart + RemoteViewServe +
RemoteViewStopAll (vea Modo Multicliente más arriba).
Ejemplos
| Archivo | Descripción |
|---|---|
samples/RemoteView/testrv.prg |
Demo mínima de cliente único: ventana + SAY + BUTTON en el puerto 8080 |
samples/RemoteView/testrv2.prg |
Ventana principal + diálogo hijo (composición de popups propios) |
samples/RemoteView/testrvmt.prg |
Multicliente: 3 servidores en los puertos 8080–8082 mediante hilos de Harbour (-mt) |
samples/RemoteView/testrvauth.prg |
Login antes de lanzar: el usuario remoto se identifica en un diálogo y luego se abre la app |
samples/RemoteView/testrvweb.prg |
Remote View + TWebView2: Edge WebView2 (HTML/JS, SendToFWH) dentro de una ventana publicada con RemoteViewStart |
samples/database/fivedburv.prg |
FiveDBU con Remote View integrado en el puerto 8080 |
Remote View + WebView2
testrvweb.prg muestra cómo embeber
TWebView2 (Microsoft Edge) en una ventana FiveWin y publicarla
con RemoteViewStart. En local: barra de botones nativos más
la superficie web (HTML demo, Navigate, Eval,
SendToFWH → bOnBind). En remoto: cualquier
navegador de la LAN abre http://host:8080 y ve/controla toda la
ventana (controles FWH y contenido WebView2 capturado con
PW_RENDERFULLCONTENT).
Disposición en la misma pantalla: sitúa la
ventana desktop abajo (FROM nTop, nLeft TO … PIXEL) para
que la página del navegador (la imagen en vivo) quede arriba. Usa un
RemoteViewSetOffset( nX, nY ) modesto para dibujar la ventana
cerca del borde superior del bitmap. Requiere Runtime WebView2 (Edge Evergreen
o Fixed Version). Compilar:
build_new.bat testrvweb hm64 (cualquier variante mono-cliente;
no hace falta -mt).
Patrón de autenticación (login antes de lanzar)
testrvauth.prg muestra cómo hacer que un usuario remoto se identifique
antes de mostrar la aplicación. Como RemoteView transmite una ventana ya en marcha
y composita sus popups owned, el login es un diálogo FiveWin normal owned
por la ventana servida — el remoto lo ve y lo rellena. Con credenciales
válidas se abre la UI de la app (también owned por la ventana servida, así aparece
al instante); si fallan, se cierra la sesión.
Cuidado — no muestres UI modal dentro de ON INIT.
Si lanzas el diálogo de login de forma síncrona en ON INIT, la ventana
anfitriona aún no ha presentado su primer frame, así que PrintWindow
(la ruta de captura) la blitea negra antes y durante el login. Difiere el
login hasta que la ventana pinte — un TTimer de un disparo va bien:
ACTIVATE WINDOW oWnd ON INIT ( RemoteViewStart( oWnd:hWnd, 8080 ), DeferLogin( oWnd ) )
STATIC FUNCTION DeferLogin( oWnd )
LOCAL oTmr
DEFINE TIMER oTmr INTERVAL 250 OF oWnd ;
ACTION ( oTmr:DeActivate(), Login( oWnd ) ) // 1 disparo: desactiva y muestra el login
ACTIVATE TIMER oTmr
RETURN NIL
En producción autentica sobre la ruta multicliente con TLS
(RemoteViewServe + -mt) y guarda las credenciales
hasheadas: la ruta single-client es HTTP/WS plano y viajarían en claro.