«Al pegar una tabla copiada de Excel, se deforma el formato; queremos que se pueda pegar como tabla.» «El contenido copiado desde nuestra aplicación se convierte en algo raro al pegarlo en Word.» «Queremos poder incorporar archivos arrastrándolos y soltándolos.» — En las consultas sobre mejoras de aplicaciones empresariales, las peticiones relacionadas con copiar y pegar y con arrastrar y soltar (D&D) son un clásico.
Precisamente por ser funciones «que se dan por sentadas», su funcionamiento interno es sorprendentemente poco conocido. Si se piensa en el portapapeles como «una caja donde se guarda un único dato», no se puede explicar por qué una misma copia da resultados distintos según dónde se pegue, ni por qué deja de poder pegarse cuando se cierra la aplicación de origen. El portapapeles real funciona colocando el mismo contenido en varios formatos a la vez, de modo que quien pega elige el formato que es capaz de entender.
Y arrastrar y soltar no es, en el fondo, más que una transferencia de datos OLE que entrega, a través de interfaces COM, exactamente la misma representación de datos que el portapapeles (IDataObject). Es decir, copiar/pegar y D&D son hermanos: entender bien uno deja al otro prácticamente al alcance de la mano.
Este artículo, dirigido a responsables de sistemas de pequeñas y medianas empresas y a desarrolladores de aplicaciones Windows, conecta en un solo hilo el mecanismo de los formatos del portapapeles, las buenas prácticas tanto del lado que pega como del lado que copia, la forma correcta de monitorizarlo, las políticas de administración del historial del portapapeles, la sincronización en la nube y RDP, y también la estructura y las trampas del arrastrar y soltar OLE.
1. Conclusión principal
- El portapapeles es una única área compartida entre las aplicaciones de un mismo escritorio (window station), y en ella no se coloca «un solo dato», sino el mismo contenido en varios formatos a la vez. Con otra sesión, como en RDP, se trata originalmente de portapapeles distintos, y es la función de redirección la que hace de puente entre ambos. Como el destino elige el formato que puede entender, una misma copia da resultados distintos según dónde se pegue. 12
- Para texto se usa CF_UNICODETEXT. CF_TEXT es ANSI y depende de la página de códigos, lo que en entornos con caracteres no latinos es fuente de mojibake. El sistema convierte implícitamente entre ambos, pero el formato de referencia es el lado Unicode. 3
- Los archivos se transfieren con CF_HDROP (un array de rutas con doble terminación NUL), y el texto con formato usa el formato registrado «HTML Format». HTML Format tiene una estructura particular: es texto UTF-8 con una cabecera de desplazamientos en bytes. 45
- La verdadera causa de que «deje de poder pegarse al cerrar la aplicación de origen» es el renderizado diferido. Es un mecanismo que, en lugar del cuerpo de los datos, coloca solo la promesa de «generarlos cuando se soliciten»; si al cerrarse no se materializan (respondiendo a WM_RENDERALLFORMATS, o llamando a OleFlushClipboard en el caso de OLE), deja de poder pegarse. 26
- Los datos que se pegan deben tratarse como una entrada externa en la que no se puede confiar. El propio Microsoft afirma explícitamente que «los datos del portapapeles no son confiables; hay que analizarlos con cuidado». 7
- Para monitorizar, la única opción es AddClipboardFormatListener + WM_CLIPBOARDUPDATE. No se usan el sondeo (polling) ni el antiguo SetClipboardViewer (cadena de visores). También existen formatos registrados (como ExcludeClipboardContentFromMonitorProcessing) para no incluir información confidencial en el historial ni en la sincronización. 81
- El historial del portapapeles (Win+V) y la sincronización en la nube son objeto de administración por parte de TI. Se controlan con AllowClipboardHistory y AllowCrossDeviceClipboard mediante GPO o Intune (Policy CSP), y la redirección del portapapeles en RDP también cuenta con una política dedicada. 91011
- Arrastrar y soltar es, en el fondo, COM. El mismo IDataObject que el portapapeles se entrega entre IDropSource (origen del arrastre) e IDropTarget (destino de la suelta) a través del bucle de DoDragDrop. RegisterDragDrop requiere obligatoriamente inicializar COM con OleInitialize (STA). 1213
- No se puede soltar sobre una aplicación elevada desde un Explorador de archivos con privilegios normales. La causa es UIPI (el bloqueo de mensajes según el nivel de integridad), una restricción que conviene conocer ya en la fase de diseño. 14
A continuación repasamos, en orden, desde los fundamentos del portapapeles.
2. El verdadero portapapeles — no es un dato único, sino el mismo contenido en varios formatos
El portapapeles es un mecanismo común de intercambio de datos accesible desde todas las aplicaciones que comparten el mismo escritorio (más exactamente, funciona por window station: una sesión de usuario distinta o una sesión RDP distinta tienen cada una su propio portapapeles. Que copiar y pegar funcione en RDP se debe a que la función de redirección hace de puente entre ambos — véase el capítulo 7). El principio fundamental es que su uso lo dirige el usuario: la política oficial de diseño es no meter ni sacar datos por cuenta propia sin que el usuario lo sepa. 1
Lo importante es que lo que coloca la operación de copiar no es «un solo dato». La ventana que copia, después de vaciar el portapapeles, coloca varios formatos seguidos con el mismo contenido, en orden del más expresivo al menos expresivo. 2 Por ejemplo, al copiar una tabla en una hoja de cálculo, conceptualmente se colocan a la vez cosas como las siguientes.
| Prioridad | Formato | Contenido |
|---|---|---|
| 1 | Formato propio de la aplicación | Representación interna completa, hasta fórmulas y formato (para pegar en la misma aplicación) |
| 2 | HTML Format | Fragmento HTML que conserva la estructura y el formato de la tabla |
| 3 | CSV | Texto separado por celdas |
| 4 | CF_UNICODETEXT | Texto plano separado por tabulaciones |
| 5 | Formato de imagen | Mapa de bits con el aspecto visual de la tabla |
El lado que pega elige de esta lista el formato que es capaz de entender y lo extrae. Que al pegar en Word se obtenga una tabla con formato, y al pegar en el Bloc de notas se obtenga texto separado por tabulaciones, se debe a que cada uno elige un formato distinto. Que «el resultado dependa de dónde se pegue» no es un error, sino la consecuencia normal de este mecanismo.
flowchart TB
accTitle: Por qué la misma copia da resultados distintos según dónde se pegue
accDescr: El lado que copia coloca el mismo contenido en varios formatos en el portapapeles, y el lado que pega elige el formato que entiende, por lo que Word obtiene una tabla con formato y el Bloc de notas obtiene texto separado por tabulaciones
copy["Lado que copia: hoja de cálculo"] --> cb["Portapapeles (mismo contenido, varios formatos)"]
cb --> f1["Formato propio de la aplicación"]
cb --> f2["HTML Format"]
cb --> f3["CSV"]
cb --> f4["CF_UNICODETEXT"]
f2 -->|"Word lo elige"| word["Tabla con formato"]
f4 -->|"El Bloc de notas lo elige"| notepad["Texto separado por tabulaciones"]
Dicho de otro modo, las consultas del inicio — «se deforma el formato», «se pega algo raro» — se reducen casi siempre a un problema de cómo un lado elige o cómo el otro ofrece los formatos. El capítulo 4 trata el lado que pega, y el capítulo 5, el lado que copia.
3. Formatos estándar y formatos registrados — CF_UNICODETEXT, CF_HDROP y HTML Format
3.1. Formatos estándar — el texto se maneja en Unicode
Los formatos que el sistema operativo define de antemano se llaman formatos estándar. Los que aparecen con más frecuencia en aplicaciones empresariales son los siguientes. 3
| Formato | Valor | Contenido |
|---|---|---|
| CF_TEXT | 1 | Texto ANSI (depende de la página de códigos) |
| CF_UNICODETEXT | 13 | Texto Unicode. Este es el formato de referencia para el texto |
| CF_HDROP | 15 | Lista de rutas de archivo (handle HDROP) |
| CF_DIB | 8 | Mapa de bits independiente del dispositivo |
| CF_LOCALE | 16 | Identificador de configuración regional asociado al texto |
CF_TEXT y CF_UNICODETEXT se convierten implícitamente entre sí en el sistema (formato sintetizado). Para esa conversión de codificación de caracteres se usa la página de códigos asociada a CF_LOCALE. 3 Confiar en la conversión hace que se pierdan los caracteres que no se pueden representar en ANSI (por ejemplo, símbolos propios de Unicode o caracteres combinados), así que el principio es unificar la lectura y escritura de la aplicación en CF_UNICODETEXT (en .NET, DataFormats.UnicodeText).
flowchart LR
accTitle: Conversión implícita entre CF_UNICODETEXT y CF_TEXT
accDescr: La aplicación solo debe leer y escribir CF_UNICODETEXT; el sistema convierte implícitamente a CF_TEXT usando la página de códigos de CF_LOCALE. Los caracteres que no se pueden representar en ANSI se pierden en esa conversión
apprw["Lectura/escritura de la aplicación"] --> uni["CF_UNICODETEXT (formato de referencia)"]
uni <-->|"El sistema convierte implícitamente (página de códigos de CF_LOCALE)"| ansi["CF_TEXT (ANSI, depende de la página de códigos)"]
ansi -.-> loss["Los caracteres no representables se pierden en la conversión (fuente de mojibake)"]
3.2. CF_HDROP — los archivos se transfieren como una lista de rutas
CF_HDROP es el formato que se usa al copiar archivos en el Explorador de archivos o al arrastrarlos y soltarlos. Su contenido no es el archivo en sí, sino un bloque de memoria que coloca un array con «doble terminación NUL»: a continuación de la cabecera de la estructura DROPFILES, las rutas completas separadas por el carácter NUL y, al final, una cadena vacía. En la cabecera, pFiles indica el desplazamiento de inicio de la lista de rutas, y fWide indica si las cadenas son Unicode. 4
[Cabecera DROPFILES: pFiles=desplazamiento de inicio de la lista de rutas, fWide=1(Unicode)]
C:\data\a.txt(NUL)C:\data\b.txt(NUL)(NUL)
En código nativo se extraen las rutas una a una con DragQueryFile, y en .NET se reciben como un string[] con DataFormats.FileDrop. El hecho de que «lo que se transfiere son solo las rutas, no el archivo en sí» también será relevante en el D&D de los capítulos 8 y 9.
flowchart TB
accTitle: Estructura del bloque de memoria de CF_HDROP
accDescr: Al principio de la memoria global hay una estructura DROPFILES, donde pFiles indica el desplazamiento de inicio de la lista de rutas y fWide indica si es Unicode. A continuación se suceden las rutas completas separadas por NUL, y el bloque termina con una cadena vacía (doble NUL). Solo viajan las rutas, no los archivos en sí
hdr["Estructura DROPFILES (pFiles = desplazamiento de inicio de la lista / fWide = 1)"] --> p1["C:\\datos\\a.txt + NUL"]
p1 --> p2["C:\\datos\\b.txt + NUL"]
p2 --> tail["Cadena vacía final (doble NUL)"]
hdr -.-> note["Solo viajan las rutas, no los archivos en sí"]
3.3. Formatos registrados — RegisterClipboardFormat y “HTML Format”
Para datos que no se pueden representar con los formatos estándar, una aplicación puede registrar un formato propio con un nombre que ella misma decida. Al pasar un nombre a RegisterClipboardFormat se obtiene un ID de formato, y si se registra con el mismo nombre, otra aplicación distinta obtiene el mismo ID, de modo que basta con acordar el nombre para compartir datos entre aplicaciones. 1 Cuando se transfieren datos estructurados entre las propias aplicaciones de una empresa, conviene usar un nombre que no choque con otros, como KomuraSoft.Report.RowData.
El representante de los formatos registrados es «HTML Format», para texto con formato (junto con RTF, uno de los dos grandes formatos de texto enriquecido). Su contenido es texto UTF-8, pero tiene una estructura particular: al principio lleva una cabecera que enumera desplazamientos en bytes. 5
Version:0.9
StartHTML:<posición en bytes de inicio del HTML completo>
EndHTML:<posición en bytes de fin del HTML completo>
StartFragment:<posición en bytes de inicio del fragmento>
EndFragment:<posición en bytes de fin del fragmento>
<html><body>
<!--StartFragment--><b>Texto en negrita</b> del fragmento<!--EndFragment-->
</body></html>
Cada desplazamiento es una posición en bytes desde el inicio de los datos, incluyendo la propia cabecera; la práctica habitual es reservar el espacio con un número fijo de dígitos (por ejemplo, 10) y, después de construir el cuerpo, volver a escribir el valor real medido. StartFragment/EndFragment indican el inicio y el fin del «fragmento que el usuario ha seleccionado realmente» en unidades de byte (no en unidades de carácter). En UTF-8 con texto en japonés (o en cualquier idioma con caracteres multibyte), el número de caracteres y el número de bytes no coinciden, así que si se calculan mal estos desplazamientos, al pegar en otra aplicación faltará contenido al principio o al final. Si se genera HTML Format por cuenta propia, hay que rellenar la cabecera con la posición en bytes medida después de codificar en UTF-8. 5
flowchart TB
accTitle: Relación entre la cabecera de HTML Format y los desplazamientos
accDescr: StartHTML y EndHTML de la cabecera señalan el HTML completo, y StartFragment y EndFragment señalan el fragmento seleccionado por el usuario, ambos como posiciones en bytes desde el inicio de los datos. Como en UTF-8 el número de caracteres y de bytes no coincide, hay que rellenar la cabecera con la posición en bytes medida después de codificar
header["Cabecera (Version / StartHTML / EndHTML / StartFragment / EndFragment)"] --> html["HTML completo (StartHTML a EndHTML)"]
html --> frag["Fragmento seleccionado (StartFragment a EndFragment)"]
header -.-> byte["Cada desplazamiento = posición en bytes desde el inicio de los datos (medida tras la codificación UTF-8)"]
Además, para datos tabulares también se usa mucho CSV (DataFormats.CommaSeparatedValue en .NET). Para la interoperabilidad con Excel, ofrecer juntos HTML Format (con formato), CSV (solo valores) y CF_UNICODETEXT (separado por tabulaciones) hace que no importe cuál sea el destino del pegado.
4. Buenas prácticas al pegar — orden de prioridad de los formatos y validación
4.1. Buscar primero los formatos más ricos
Los formatos del portapapeles están ordenados según el orden en que los colocó el lado que copia (es decir, del más expresivo al menos). La base del lado que pega es buscar, entre los formatos que puede manejar, empezando por el que tiene más información. En Win32 se puede enumerar con EnumClipboardFormats y usar el primer formato reconocido, o bien pasar la propia lista de prioridades a GetPriorityClipboardFormat para elegir. 2
.NET usaría una ramificación como la siguiente.
// Pegado de tabla: buscar en orden de más rico a más plano
var data = Clipboard.GetDataObject();
if (data is null) return;
// Aunque un formato diga que existe, el contenido real no siempre es string.
// Usar esta rama solo si se puede confirmar el tipo; si no, caer al siguiente candidato
if (data.GetDataPresent(DataFormats.Html)
&& data.GetData(DataFormats.Html) is string html)
{
// Validar la cabecera de HTML Format y luego incorporar como tabla
}
else if (data.GetDataPresent(DataFormats.CommaSeparatedValue))
{
// Incorporar como CSV
}
else if (data.GetDataPresent(DataFormats.UnicodeText))
{
// Incorporar como texto separado por tabulaciones
}
Esta es la respuesta a la consulta del inicio: «al pegar una tabla de Excel se deforma». A una aplicación que solo lee texto plano no le llega la estructura de la tabla. Hasta qué formato se acepta es una decisión de diseño del lado que pega.
flowchart TB
accTitle: Ramas de decisión al pegar, probando primero los formatos más ricos
accDescr: Si existe HTML Format y su contenido real es una cadena, se incorpora como tabla; si no, se prueba CSV; si tampoco existe, se prueba texto separado por tabulaciones; y si no hay ningún candidato, no se acepta
startsel["Inicio del pegado"] --> h{"¿Existe HTML Format y el contenido real es string?"}
h -->|"Sí"| useh["Validar la cabecera e incorporar como tabla"]
h -->|"No"| c{"¿Existe CSV?"}
c -->|"Sí"| usec["Incorporar como CSV"]
c -->|"No"| t{"¿Existe UnicodeText?"}
t -->|"Sí"| uset["Incorporar como texto separado por tabulaciones"]
t -->|"No"| giveup["No se acepta"]
4.2. Los datos pegados son una entrada externa
Es algo que se suele pasar por alto, pero el contenido del portapapeles es un dato de origen externo del que no se sabe qué aplicación lo colocó. La propia Microsoft, en la documentación del portapapeles OLE, advierte que «los datos del portapapeles no son confiables; hay que analizarlos con cuidado antes de usarlos en la aplicación». 7
- Validar que los desplazamientos de la cabecera de HTML Format no apunten fuera de rango (existen aplicaciones que generan cabeceras corruptas).
- Los valores que se incorporan como número, fecha o código deben pasar la misma validación que la entrada por pantalla.
- Incorporar defensas contra datos enormes. Aunque se peguen imágenes de cientos de megabytes o texto de millones de líneas, la interfaz no debe bloquearse, y hay que diseñar el sistema para que rechace el contenido cuando se supere un límite. Un punto a tener en cuenta: como GetData en .NET, en el momento en que se llama, materializa (activando incluso el renderizado diferido) todos los datos hasta convertirlos en una cadena administrada, colocar la comprobación de tamaño después de GetData no sirve de defensa. En Win32, comprobar con GlobalSize el HGLOBAL que devuelve GetClipboardData permite una defensa en la etapa de «no avanzar con datos enormes hacia la conversión y el análisis en una cadena administrada», pero en los formatos con renderizado diferido, el propio GetClipboardData ya dispara el renderizado, así que esto no evita la materialización en sí en el origen de la copia. Para no bloquear la interfaz, hay que sacar el procesamiento de obtención fuera del hilo de interfaz (en ese caso, como el Clipboard de .NET también exige STA, se hace en un hilo dedicado configurado como STA, no en el grupo de hilos (MTA) de Task.Run — sección 5.1).
La idea de que «un valor que llega desde fuera, sea cual sea el canal, debe validarse antes de usarse» es la misma que se planteó en «No hay que usar directamente el valor leído de un código QR». Dar por hecho que pegar es seguro porque es una acción del usuario es la fuente del problema.
flowchart LR
accTitle: Flujo de validación de los datos pegados antes de usarlos
accDescr: Los datos que se extraen del portapapeles pasan, en orden, por la comprobación de existencia del formato, la comprobación del tipo real, el límite de tamaño y la validación del contenido; si algo falla en cualquier punto, se rechaza o se cae al siguiente formato candidato
present["Comprobar que existe el formato (GetDataPresent)"] --> type["Comprobar el tipo real (is string / string[])"]
type --> size["Comprobar el límite de tamaño"]
size --> content["Validar el contenido (cabecera, ruta, valor)"]
content --> ok["Incorporar"]
type -.->|"Tipo incorrecto"| rej["Rechazar / pasar al siguiente formato candidato"]
size -.->|"Demasiado grande"| rej
content -.->|"No válido"| rej
5. Buenas prácticas al copiar — ofrecer varios formatos a la vez y renderizado diferido
5.1. Colocar varios formatos a la vez
La buena práctica del lado que copia es el reverso de la 4.1: ofrecer a la vez un formato rico y un formato plano. Con el DataObject de WinForms/WPF se puede escribir en pocas líneas. 15
// WinForms (System.Windows.Forms). WPF usa el mismo tipo con DataObject/Clipboard de System.Windows
var data = new DataObject();
data.SetData(DataFormats.Html, htmlFormatText); // Cadena HTML Format con cabecera
data.SetData(DataFormats.CommaSeparatedValue, csv); // CSV
data.SetData(DataFormats.UnicodeText, plainText); // Texto plano
Clipboard.SetDataObject(data, copy: true); // copy:true = se conserva tras cerrar la aplicación
Dos aclaraciones. En primer lugar, la clase Clipboard de .NET solo se puede usar desde un hilo STA. 15 El hilo de interfaz de WinForms/WPF es STA gracias a [STAThread], así que normalmente no hay problema, pero si se intenta usar desde un hilo en segundo plano falla (para los fundamentos de STA/MTA, véase «Fundamentos de STA/MTA en COM»). En segundo lugar, el significado de copy: true está relacionado con el renderizado diferido del siguiente apartado.
5.2. Renderizado diferido — por qué no se puede pegar después de cerrar el origen
Como generar datos grandes en muchos formatos cada vez es un desperdicio, el portapapeles cuenta con un mecanismo llamado renderizado diferido (delayed rendering). Si se pasa NULL como handle de datos a SetClipboardData, en lugar del cuerpo de los datos se registra solo la promesa de «generarlos cuando se soliciten»; en el momento en que alguien solicita ese formato, le llega al origen de la copia el mensaje WM_RENDERFORMAT, y solo entonces se generan los datos. 2
La consecuencia de este mecanismo es el «dejó de poder pegarse al cerrar la aplicación de origen» del inicio. Antes de cerrarse, el origen recibe WM_RENDERALLFORMATS y tiene la responsabilidad de materializar todos los formatos aún no renderizados; si se cierra sin cumplir esta responsabilidad, ese formato se pierde. 2
flowchart TB
accTitle: Flujo del renderizado diferido y por qué no se puede pegar tras cerrar el origen
accDescr: El origen solo registra una promesa con un handle NULL, y cuando llega una solicitud la materializa mediante WM_RENDERFORMAT. Al cerrarse tiene la obligación de materializar todos los formatos con WM_RENDERALLFORMATS, y si no lo hace, ese formato se pierde
promise["Origen: SetClipboardData(formato, NULL) registra solo una 'promesa'"] --> req["El lado que pega solicita ese formato"]
req --> render["WM_RENDERFORMAT → genera los datos en ese momento"]
promise --> quit["El origen intenta cerrarse"]
quit -->|"Materializa con WM_RENDERALLFORMATS"| ok["Se puede pegar incluso después de cerrarse"]
quit -->|"Omite la materialización"| lost["Ese formato se pierde ('no se puede pegar tras cerrar')"]
En el portapapeles OLE (el método que coloca IDataObject con OleSetClipboard), esta relación es todavía más explícita. Lo único que retiene el portapapeles es un puntero al objeto de datos, y al llamar a OleFlushClipboard cuando la aplicación se cierra, los datos se materializan en el portapapeles y quedan disponibles para pegar incluso después del cierre. 6 El Clipboard.SetDataObject(data, copy: true) de .NET es precisamente lo que especifica este comportamiento de «conservar después de cerrarse».
Cuando en Excel se copia un rango grande y se intenta cerrar la aplicación, el mensaje «Hay mucha información en el Portapapeles. ¿Desea conservarla?» es precisamente la confirmación de si se ejecuta o no este procesamiento de confirmación (el flush). Si se usa renderizado diferido en una aplicación propia, hay que recordar que el procesamiento de confirmación al cerrarse forma un único conjunto con la parte de registrar la promesa. Cabe señalar que el renderizado diferido es una optimización de rendimiento, y como la solicitud de renderizado se ejecuta de forma síncrona dentro del procesamiento de mensajes, existe también la contrapartida de que, con datos que tardan en generarse, la interfaz se puede quedar bloqueada. 2
6. Buenas prácticas para monitorizar el portapapeles — listener, reintentos y exclusión del historial
6.1. Usar AddClipboardFormatListener
En requisitos como «queremos detectar automáticamente el valor de un lector de códigos de barras o una copia hecha desde el sistema troncal, y capturarlo», hace falta monitorizar los cambios del portapapeles. Históricamente hay tres métodos, pero la respuesta correcta hoy es una sola. 8
| Método | Evaluación |
|---|---|
| Leer periódicamente con un temporizador (polling) | Poco eficiente y con detecciones que se escapan. No usar |
| SetClipboardViewer (cadena de visores) | Un fallo en una sola aplicación de la cadena rompe el conjunto. Se mantiene solo por compatibilidad hacia atrás |
| AddClipboardFormatListener | Recomendado. A la ventana registrada le llega WM_CLIPBOARDUPDATE |
flowchart LR
accTitle: Flujo de la monitorización del portapapeles
accDescr: Al registrar con AddClipboardFormatListener en la creación del handle, llega WM_CLIPBOARDUPDATE sin importar qué aplicación copie. La lectura se hace con reintentos, y al destruir el handle se cancela el registro de forma simétrica con RemoveClipboardFormatListener
created["OnHandleCreated: AddClipboardFormatListener"] --> wait["En espera"]
anyapp["Cualquier aplicación copia algo"] --> notify["Llega WM_CLIPBOARDUPDATE"]
wait --> notify
notify --> readtry["Lectura con reintentos (sección 6.2)"]
readtry --> wait
destroyed["OnHandleDestroyed: RemoveClipboardFormatListener"] -.->|"Cancelación simétrica"| created
// Implementación mínima en WinForms
public partial class MainForm : Form
{
[DllImport("user32.dll", SetLastError = true)]
static extern bool AddClipboardFormatListener(IntPtr hwnd);
[DllImport("user32.dll", SetLastError = true)]
static extern bool RemoveClipboardFormatListener(IntPtr hwnd);
const int WM_CLIPBOARDUPDATE = 0x031D;
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
AddClipboardFormatListener(Handle);
}
protected override void OnHandleDestroyed(EventArgs e)
{
// Cancelar el registro de forma simétrica a la creación/recreación del handle
RemoveClipboardFormatListener(Handle);
base.OnHandleDestroyed(e);
}
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_CLIPBOARDUPDATE)
{
// Aquí se lee Clipboard.GetDataObject() y, si el formato es el necesario, se incorpora
}
base.WndProc(ref m);
}
}
6.2. Reintentar cuando no se puede abrir
El portapapeles solo lo puede abrir una ventana a la vez, y mientras otro proceso lo tiene abierto, OpenClipboard falla. 2 Justo después de recibir WM_CLIPBOARDUPDATE es, precisamente, muy probable que el origen de la copia u otra aplicación de monitorización estén operando en ese momento, así que es normal que la lectura falle temporalmente. Incluya siempre varios reintentos con una espera breve (unas decenas de milisegundos) entre medias. Además, en la clase Clipboard de .NET, la sobrecarga que permite especificar el número de reintentos y el intervalo existe solo en el lado de escritura, SetDataObject. En el lado de lectura (GetDataObject y similares) no existe, así que hay que escribir uno mismo el código que capture ExternalException en WinForms o COMException en WPF y espere para reintentar.
flowchart LR
accTitle: Flujo de reintentos al leer el portapapeles
accDescr: Como solo una ventana puede abrir el portapapeles a la vez, la lectura justo después de la notificación de cambio puede competir con otro proceso y fallar. Si se recibe una excepción, se espera unas decenas de milisegundos y se reintenta, y al llegar al límite se desiste por esta vez y se espera a la siguiente actualización
upd["WM_CLIPBOARDUPDATE"] --> tryread["Intentar leer"]
tryread -->|"Éxito"| useok["Pasar a la incorporación (validación del capítulo 4)"]
tryread -->|"Falla (otra ventana lo está usando)"| waitretry["Esperar unas decenas de milisegundos"]
waitretry -->|"Reintentar hasta varias veces"| tryread
waitretry -->|"Se alcanza el límite"| giveup2["Desistir por esta vez (se recogerá en la siguiente actualización)"]
6.3. No incluir en el historial ni la sincronización — consideraciones para funciones de copia con datos confidenciales
Windows tiene un historial del portapapeles (Win+V) y una sincronización entre dispositivos (portapapeles en la nube), y los datos que coloca una aplicación son, por defecto, objeto de ambos. Las aplicaciones que ponen en la función de copia información confidencial, como contraseñas o números de cuenta, deben colocar junto a los datos un formato registrado para excluirlos del historial y de la sincronización. 1
- ExcludeClipboardContentFromMonitorProcessing: si se coloca este formato, todo el contenido de esa copia queda excluido tanto del historial como de la sincronización.
- CanIncludeInClipboardHistory (DWORD 0): suprime solo el historial.
- CanUploadToCloudClipboard (DWORD 0): suprime solo la sincronización entre dispositivos.
Que los gestores de contraseñas no dejen la contraseña copiada en Win+V se debe a este mecanismo. Basta con pasar el nombre a RegisterClipboardFormat para obtener el ID de formato y colocarlo junto a los datos normales, así que en aplicaciones empresariales que manejan información confidencial vale la pena implementarlo.
7. El portapapeles desde la perspectiva de TI — control del historial, la sincronización en la nube y RDP
Alejándonos un poco del desarrollo, ordenemos los puntos desde la perspectiva del administrador. El historial del portapapeles acumula el contenido copiado más reciente, y el portapapeles en la nube sincroniza el contenido copiado entre dispositivos que han iniciado sesión con la misma cuenta Microsoft o cuenta de Microsoft Entra. 10 Aunque es cómodo, se producen situaciones de permanencia y traspaso de información, como que información personal copiada del sistema troncal se acumule en el historial, o que contenido copiado en el PC de trabajo se sincronice con un PC personal.
Las políticas que puede controlar la organización son las dos siguientes.
| Objeto de control | GPO (Configuración del equipo > Plantillas administrativas > Sistema > Directivas del SO) | Policy CSP (Intune) | Valor predeterminado |
|---|---|---|---|
| Historial del portapapeles | Permitir el historial del Portapapeles | Experience/AllowClipboardHistory | Permitido |
| Sincronización entre dispositivos | Permitir la sincronización del Portapapeles entre dispositivos | Privacy/AllowCrossDeviceClipboard | Permitido |
Ambas están disponibles desde Windows 10 versión 1809 en adelante; al deshabilitarlas, el elemento correspondiente en la aplicación Configuración queda en gris, y la política se aplica de inmediato. 910
Otro punto clásico es la redirección del portapapeles en RDP (Escritorio remoto). Por defecto, copiar y pegar funciona entre el PC local y la sesión remota, lo que puede convertirse en una vía de fuga de información confidencial del servidor. Con la política «No permitir la redirección del Portapapeles» (el valor de registro fDisableClip) se pueden bloquear ambas direcciones. 11 En versiones recientes de Windows Server/Windows 11 también se han añadido políticas de control más granular, como restringir solo la dirección del servidor al cliente a texto únicamente. Si se opta por una prohibición total o por una restricción por niveles es algo que se decide según el equilibrio entre operatividad y seguridad.
flowchart LR
accTitle: Rutas por las que se propaga el contenido del portapapeles y sus puntos de control
accDescr: El contenido copiado entra por defecto en el historial y la sincronización en la nube, y con RDP pasa a otra sesión mediante la redirección. Cada ruta se puede controlar con una política, y del lado de la aplicación se puede excluir del historial y la sincronización con un formato de exclusión
cb["Portapapeles"] --> hist["Historial (Win+V)"]
cb --> cloud["Sincronización en la nube → otro dispositivo"]
cb --> rdp["Redirección RDP → otra sesión"]
hist -.-> p1["Control: AllowClipboardHistory"]
cloud -.-> p2["Control: AllowCrossDeviceClipboard"]
rdp -.-> p3["Control: fDisableClip"]
cb -.-> p4["Lado de la aplicación: excluir con ExcludeClipboardContentFromMonitorProcessing, etc. (sección 6.3)"]
8. Arrastrar y soltar es, en realidad, COM — IDataObject + IDropSource + IDropTarget
8.1. Los mismos datos que el portapapeles, transportados de otra manera
El arrastrar y soltar OLE funciona con estos tres actores. 12
| Rol | Quién lo implementa | Función |
|---|---|---|
| IDataObject | Origen del arrastre | El cuerpo de los datos transportados. El mismo objeto de datos con varios formatos que el portapapeles |
| IDropSource | Origen del arrastre | Decidir si continuar o cancelar el arrastre y dar retroalimentación de cursor |
| IDropTarget | Destino de la suelta | Declarar si se acepta y recibir los datos mediante DragEnter/DragOver/DragLeave/Drop |
Cuando el origen del arrastre llama a DoDragDrop, comienza el bucle de arrastre; cuando el ratón entra en la ventana de destino de la suelta, llega la notificación a su IDropTarget, y en el momento de soltar se entrega el IDataObject. La documentación oficial también dice que «D&D ofrece exactamente la misma funcionalidad que copiar y pegar del portapapeles: en una aplicación que ya tiene implementado copiar/pegar, añadirlo cuesta muy poco». 12 Es decir, el DataObject con varios formatos que se construyó en los capítulos 2 a 5 se convierte, tal cual, en la carga del D&D.
flowchart LR
accTitle: Flujo de arrastrar y soltar OLE
accDescr: El origen del arrastre llama a DoDragDrop con IDataObject como carga y comienza el bucle de arrastre; el IDropTarget del destino declara si acepta mediante DragEnter y DragOver, y en Drop extrae el formato que necesita de IDataObject
src["Origen del arrastre: IDataObject + IDropSource"] -->|"DoDragDrop"| loop["Bucle de arrastre"]
loop -->|"El puntero entra en la ventana"| enter["IDropTarget.DragEnter/DragOver (declara Effect cada vez)"]
enter -->|"Se suelta el botón"| drop["IDropTarget.Drop"]
drop --> data["Extrae el formato que necesita de IDataObject"]
8.2. OleInitialize (STA) es obligatorio
La ventana que va a ser destino de la suelta se registra con RegisterDragDrop, pero aquí hay una trampa clásica. Si la inicialización de COM se hace con CoInitialize/CoInitializeEx, RegisterDragDrop falla siempre con E_OUTOFMEMORY, y hay que inicializar con OleInitialize. 13 Esto se debe a que OleInitialize inicializa COM como STA, y D&D es una función que está arraigada en el mundo STA de ventanas y bucle de mensajes. También es obligatorio que el hilo que hace la llamada esté ejecutando el bucle de mensajes; si no lo hace, otras aplicaciones se quedan colgadas mientras dura el arrastre. 13 El trasfondo de todo esto es exactamente el tema del modelo de hilos tratado en «Fundamentos de STA/MTA en COM».
En aplicaciones WinForms/WPF, el framework se encarga de la inicialización de OLE y de la implementación de las interfaces, así que el desarrollador solo necesita escribir los controladores de eventos.
// WinForms: recibir archivos soltados
listView1.AllowDrop = true;
listView1.DragEnter += (s, e) =>
{
// Comprobar también que el origen permite Copy (algunos orígenes solo permiten Move/Link)
e.Effect = e.Data.GetDataPresent(DataFormats.FileDrop)
&& (e.AllowedEffect & DragDropEffects.Copy) == DragDropEffects.Copy
? DragDropEffects.Copy // Se acepta: recibir como copia
: DragDropEffects.None; // No se acepta
};
listView1.DragDrop += (s, e) =>
{
// Los datos arrastrados también son una entrada externa. Aunque el formato
// diga FileDrop, el contenido real puede ser null o de otro tipo, y la propia obtención puede fallar
object data;
try { data = e.Data.GetData(DataFormats.FileDrop); }
catch (COMException) { return; }
if (data is not string[] paths) return;
foreach (var path in paths)
{
// Validar la ruta antes de incorporarla (sección 9.3)
}
};
En WPF el planteamiento es el mismo: se recibe con AllowDrop="True" en el elemento y los eventos DragOver/Drop, y se extrae el array de rutas con e.Data.GetData(DataFormats.FileDrop). La costumbre de IDropTarget es declarar en cada DragEnter/DragOver si se acepta o no (Effect); si se omite este paso, se produce el fallo de que el cursor se queda en «prohibido» sin cambiar nunca.
9. Trampas de D&D — elevación de privilegios, Move y validación de rutas
9.1. No se puede soltar sobre una aplicación elevada como administrador
Que una aplicación ejecutada «como administrador» no reaccione al soltar un archivo desde el Explorador de archivos no es un error de implementación, sino una característica del sistema operativo. Se debe a que UIPI (User Interface Privilege Isolation) bloquea por defecto los mensajes de un proceso de nivel de integridad bajo hacia una ventana de nivel de integridad alto, así que la notificación de suelta no llega desde el Explorador de archivos con privilegios normales (integridad media) hasta una aplicación elevada. 14
flowchart LR
accTitle: Cómo UIPI bloquea la suelta sobre una aplicación elevada
accDescr: La notificación de suelta desde el Explorador (integridad media) hacia una aplicación elevada (integridad alta) es bloqueada por defecto por UIPI y no llega. Manteniendo la interfaz con privilegios normales y separando el procesamiento con privilegios, la suelta sí llega
explorer["Explorador (integridad media)"] -->|"Notificación de suelta"| uipi{"UIPI"}
uipi -->|"Bloquea (por defecto)"| elevated["Aplicación elevada (integridad alta): sin respuesta"]
uipi -->|"Pasa"| normal["UI con privilegios normales: la suelta llega"]
normal -.->|"Solo delega el procesamiento con privilegios"| broker["Proceso separado para el procesamiento que requiere elevación"]
Se conoce una solución alternativa que permite mensajes concretos como WM_DROPFILES de forma individual con ChangeWindowMessageFilterEx 14, pero lo que se habilita con esto es la notificación de suelta del estilo antiguo (WM_DROPFILES), y no resuelve el D&D OLE en su conjunto. La pauta práctica es clara: dejar de diseñar la aplicación para que se ejecute siempre elevada. Si solo el procesamiento que requiere elevación se separa en otro proceso, la interfaz principal puede seguir con privilegios normales y recibir el D&D (el diseño de esta separación se detalla en «Privilegios de administrador y procesos broker en aplicaciones Windows»).
9.2. El significado de DragDropEffects — Move es un contrato en el que “el origen desaparece”
Copy/Move/Link de DragDropEffects no son un adorno, sino un contrato entre el origen del arrastre y el destino de la suelta. El origen del arrastre declara en DoDragDrop el conjunto de efectos que permite, el destino de la suelta elige el efecto real, y la convención es que cuando se concreta Move, el origen del arrastre borra los datos (el archivo). Si el lado receptor devuelve Move sin pensarlo demasiado, se produce el accidente de «al soltar, desapareció el archivo original». En usos de incorporación en aplicaciones empresariales, la opción segura por defecto es que el lado receptor declare explícitamente Copy.
flowchart LR
accTitle: El contrato de DragDropEffects — con Move el origen desaparece
accDescr: El origen del arrastre declara en DoDragDrop el conjunto de efectos que permite, y el destino elige el efecto real. Como la convención es que si se concreta Move el origen borra el archivo, el destino debe declarar Copy explícitamente en los usos de incorporación
srcdecl["Origen del arrastre: declara los efectos que permite (Copy | Move | Link)"] --> tgtsel["Destino: elige el efecto real"]
tgtsel -->|"Copy"| copyok["El archivo original permanece (opción segura para incorporación)"]
tgtsel -->|"Move"| moveact["El origen borra el archivo — la causa del accidente de 'el original desapareció'"]
9.3. Validación de las rutas soltadas
Con CF_HDROP/FileDrop lo que se transfiere son solo rutas (sección 3.2). Antes de incorporarlas, hay que pasarlas por la misma validación como entrada externa que se aplica al pegar.
- Archivo o carpeta: decidir de antemano, como parte de la especificación, qué pasa cuando se suelta una carpeta entera (incorporarla de forma recursiva o rechazarla).
- Marcadores de posición de OneDrive: cuando se trata de un «archivo bajo demanda» cuya ruta existe pero cuyo cuerpo no está en local, al abrirlo se dispara la descarga en ese mismo instante, y falla si no hay conexión. Para el comportamiento y las medidas al respecto, véase «OneDrive: Archivos bajo demanda y las aplicaciones empresariales».
- Rutas largas o especiales: las rutas que superan MAX_PATH, las rutas de red (UNC) y las rutas en medios extraíbles deben aceptarse solo después de comprobar que el procesamiento posterior puede manejarlas.
- Número de elementos y tamaño total: para que la interfaz no se bloquee al soltar miles de archivos, la incorporación debe hacerse de forma asíncrona, con límites y una indicación de progreso.
10. Resumen
- El portapapeles es un mecanismo que coloca el mismo contenido en varios formatos a la vez, en una única área compartida dentro del mismo escritorio (window station). Como el destino elige el formato, una misma copia da resultados distintos.
- El texto usa CF_UNICODETEXT, los archivos usan CF_HDROP, y el texto con formato usa el formato registrado HTML Format (cabecera de desplazamientos en bytes + UTF-8).
- El lado que pega busca los formatos en orden de más rico a más plano, y valida el contenido como entrada externa. El lado que copia ofrece varios formatos a la vez, y si usa renderizado diferido, implementa también la confirmación al cerrarse (WM_RENDERALLFORMATS/OleFlushClipboard).
- Para monitorizar: AddClipboardFormatListener + WM_CLIPBOARDUPDATE. Frente a la competencia por OpenClipboard hay que prepararse con reintentos, y la información confidencial se excluye del historial y la sincronización con ExcludeClipboardContentFromMonitorProcessing y similares.
- TI puede controlar el historial del portapapeles, la sincronización en la nube y la redirección RDP mediante GPO/Intune. Por defecto todo está permitido, así que en entornos que manejan información confidencial conviene decidirlo de forma consciente.
- El arrastrar y soltar es, en el fondo, COM: IDropSource/IDropTarget se entregan el mismo IDataObject que el portapapeles. RegisterDragDrop requiere obligatoriamente OleInitialize (STA).
- La suelta sobre una aplicación elevada queda bloqueada por UIPI. Move en DragDropEffects es un contrato en el que «el original desaparece», y las rutas soltadas deben validarse antes de incorporarlas.
Copiar/pegar y D&D son, para el usuario, funciones tan naturales como el aire que respira. Precisamente por eso, cuando ocurre «no se puede pegar», «se deforma» o «desapareció», el deterioro de la experiencia es grande; y, a la inversa, una aplicación en la que la oferta de varios formatos y el soporte de la suelta están bien cuidados hace que el trabajo diario resulte, solo por eso, mucho más fluido. Espero que esto sirva como criterio a la hora de decidir las prioridades de una mejora.
Artículos relacionados
- ¿Qué son COM, ActiveX y OCX? — Diferencias y relación explicadas
- Fundamentos de STA/MTA en COM — El modelo de hilos y cómo evitar cuelgues
- La integración con el shell de Windows hoy — Menú contextual, asociación de archivos y los cambios en Windows 11
- El problema de que EXCEL.EXE no se cierra al automatizar Excel desde C# — Patrones para liberar referencias COM y cuándo sustituirlas
- Diseño de UX para aplicaciones Windows — Prioridades según el entorno de uso
- OneDrive «Archivos bajo demanda» y las aplicaciones empresariales — Las suposiciones que rompen los marcadores de posición y cómo abordarlas
Áreas de consultoría relacionadas
En KomuraSoft LLC nos encargamos del diseño e implementación del soporte de copiar y pegar y de arrastrar y soltar en aplicaciones empresariales (ofrecer varios formatos, integración con Excel, incorporación mediante suelta de archivos), la investigación de la causa de fallos como «se deforma al pegar» o «la copia desaparece», la automatización de la entrada mediante monitorización del portapapeles y la implementación de medidas para excluir información confidencial del historial y la sincronización. También nos ocupamos de proyectos que involucran las capas de bajo nivel de COM u OLE, aunque sea empezando por la simple identificación del fenómeno.
- Desarrollo de aplicaciones Windows
- Desarrollo de componentes COM
- Consultoría técnica y revisión de diseño
- Contacto
Referencias
-
Microsoft Learn, Clipboard Formats. Sobre que una ventana puede colocar la misma información en varios formatos de portapapeles, los formatos registrados mediante RegisterClipboardFormat (un registro con el mismo nombre devuelve el mismo valor y permite compartirlo entre aplicaciones), los formatos sintetizados, y la exclusión del historial del portapapeles y de la sincronización en la nube mediante ExcludeClipboardContentFromMonitorProcessing, CanIncludeInClipboardHistory y CanUploadToCloudClipboard. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Clipboard Operations. Sobre que solo una ventana puede tener abierto el portapapeles a la vez, que al copiar se colocan los formatos en orden del más al menos expresivo, la selección de formato al pegar mediante EnumClipboardFormats/GetPriorityClipboardFormat, el renderizado diferido al pasar NULL a SetClipboardData y la responsabilidad de WM_RENDERFORMAT/WM_RENDERALLFORMATS, y la contrapartida del renderizado diferido. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
Microsoft Learn, Standard Clipboard Formats. Sobre la definición de los formatos estándar como CF_TEXT (ANSI), CF_UNICODETEXT, CF_HDROP, CF_DIB y CF_LOCALE, y sobre la conversión implícita entre CF_TEXT y CF_UNICODETEXT que realiza el sistema usando la página de códigos asociada a CF_LOCALE. ↩ ↩2 ↩3
-
Microsoft Learn, Shell Clipboard Formats. Sobre que CF_HDROP se compone de la estructura DROPFILES y un array de cadenas de ruta completa con doble terminación NUL, la extracción de rutas individuales con DragQueryFile, y que los formatos de shell de la familia CFSTR_ requieren registro mediante RegisterClipboardFormat. ↩ ↩2
-
Microsoft Learn, HTML Clipboard Format. Sobre que el nombre registrado es «HTML Format», la estructura de la cabecera con desplazamientos (en bytes) como Version, StartHTML, EndHTML, StartFragment y EndFragment, que la codificación es siempre UTF-8, y la convención de los comentarios StartFragment/EndFragment. ↩ ↩2 ↩3
-
Microsoft Learn, OleFlushClipboard function (ole2.h). Sobre que con OleSetClipboard el portapapeles solo retiene un puntero al objeto de datos, que OleFlushClipboard materializa los datos en el portapapeles y permite pegar después de que la aplicación se cierre, y que si no hace falta conservarlos al cerrarse conviene vaciar el portapapeles con OleSetClipboard(NULL). ↩ ↩2
-
Microsoft Learn, OleGetClipboard function (ole2.h). Sobre el método para obtener un IDataObject del portapapeles y la advertencia de que «los datos del portapapeles no son confiables, por lo que la aplicación debe analizarlos con cuidado antes de usarlos». ↩ ↩2
-
Microsoft Learn, Using the clipboard. Sobre la comparación de los tres métodos de monitorización del portapapeles (ventana visor, número de secuencia y listener de formato), que los programas nuevos deben usar un listener mediante AddClipboardFormatListener, que la cadena de visores es vulnerable a fallos en el mantenimiento de la cadena, y que el número de secuencia no debe usarse para hacer polling. ↩ ↩2
-
Microsoft Learn, Policy CSP - Experience. Sobre cómo la política Experience/AllowClipboardHistory permite o prohíbe el historial del portapapeles, que está disponible desde Windows 10 versión 1809 en adelante, que el valor predeterminado es permitido, y que en GPO se mapea bajo «Sistema > Directivas del SO» y los cambios se aplican de inmediato. ↩ ↩2
-
Microsoft Learn, Policy CSP - Privacy. Sobre cómo la política Privacy/AllowCrossDeviceClipboard permite o prohíbe la sincronización del portapapeles entre dispositivos, que la sincronización se realiza entre dispositivos que iniciaron sesión con la misma cuenta Microsoft o cuenta de Microsoft Entra, y que el valor predeterminado es permitido. ↩ ↩2 ↩3
-
Microsoft Learn, Policy CSP - ADMX_TerminalServer. Sobre cómo TS_CLIENT_CLIPBOARD («No permitir la redirección del Portapapeles», valor de registro fDisableClip) permite prohibir el uso compartido del portapapeles entre local y remoto en una sesión de Escritorio remoto, y que por defecto la redirección está permitida. ↩ ↩2
-
Microsoft Learn, Drag and Drop (COM). Sobre que el arrastrar y soltar OLE funciona con tres actores — IDropSource (origen del arrastre), IDropTarget (destino de la suelta) y DoDragDrop (el bucle que ofrece OLE) —, que ofrece la misma funcionalidad que copiar y pegar del portapapeles y que en una aplicación que ya tiene copiar/pegar añadirlo cuesta poco, y sobre los tipos de retroalimentación. ↩ ↩2 ↩3
-
Microsoft Learn, RegisterDragDrop function (ole2.h). Sobre el método de registro de la ventana de destino de la suelta y de IDropTarget, que si COM se inicializó con CoInitialize/CoInitializeEx, siempre falla con E_OUTOFMEMORY y hace falta OleInitialize, y que si el hilo que hace la llamada no ejecuta el bucle de mensajes, la aplicación de origen del arrastre se cuelga. ↩ ↩2 ↩3
-
Microsoft Learn, ChangeWindowMessageFilterEx function (winuser.h). Sobre que UIPI es un mecanismo de seguridad que bloquea por defecto la recepción de mensajes procedentes de un origen de nivel de integridad bajo, y que con un filtro de mensajes a nivel de ventana se pueden permitir mensajes concretos (MSGFLT_ALLOW). ↩ ↩2 ↩3
-
Microsoft Learn, How to add data to the Clipboard (Windows Forms). Sobre el método de colocar datos en varios formatos a la vez con DataObject y Clipboard.SetDataObject, que conviene añadir varios formatos para que otras aplicaciones puedan reconocerlos, y que la clase Clipboard solo se puede usar desde un hilo STA, para lo cual hace falta [STAThread]. ↩ ↩2
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Externalización y desarrollo por encargo de una app Windows: qué aclarar antes de solicitarlo
Antes de encargar la externalización o el desarrollo por encargo de una app Windows, estos son los puntos a aclarar: revisión de software...
Introducción a la accesibilidad en aplicaciones Windows — UI Automation y cómo prepararse para la obligatoriedad del ajuste razonable
Ante la reforma legal japonesa vigente desde abril de 2024, explicamos UI Automation, el mecanismo con el que los lectores de pantalla le...
Integrar la autenticación de Entra ID en aplicaciones WinForms/WPF — Configuración práctica con MSAL.NET y el bróker WAM
Cómo integrar Entra ID en apps WinForms/WPF: cliente público, registro de la app, AcquireTokenSilent, bróker WAM y persistencia de la cac...
Pruebas de UI automatizadas para aplicaciones de escritorio de Windows ── el funcionamiento de UI Automation y pruebas resistentes a roturas con FlaUI
Organizamos las pruebas de UI automatizadas para apps WinForms/WPF desde el funcionamiento de Windows UI Automation: implementación mínim...
Cómo elegir entre WinForms, WPF y WinUI: tabla de decisión práctica
Analizamos qué elegir entre WinForms, WPF y WinUI según el desarrollo nuevo, los activos existentes, la distribución, la expresividad de ...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Migración de ActiveX
Decisiones para conservar, encapsular o sustituir componentes COM / ActiveX / OCX.
Hilo de UI y temporizadores
Hilo de UI de WPF / WinForms, flujos asíncronos, Dispatcher y diseño de temporizadores.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
Aplicaciones empresariales, integración de dispositivos y herramientas de comunicación, de los requisitos al desarrollo.
Reutilización y migración de activos existentes
Reutilización y migración de activos COM / ActiveX / OCX y dependencias de 32 o 64 bits.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- ¿Por qué se deforma el formato de una tabla copiada de Excel al pegarla en una aplicación?
- El portapapeles no contiene «un solo dato», sino el mismo contenido colocado simultáneamente en varios formatos (el formato propio de la aplicación, HTML Format, CSV, texto Unicode, etc.), y la aplicación de destino extrae el formato que es capaz de entender. Cuando el formato se deforma, lo típico es que el destino solo lea texto plano (CF_UNICODETEXT). Si se quiere conservar también la estructura de la tabla, hay que implementar en el lado que pega una extracción que dé prioridad a HTML Format o al formato CSV. A la inversa, si se quiere que otras aplicaciones peguen correctamente el contenido copiado desde la propia aplicación, hay que ofrecer al mismo tiempo, en el momento de copiar, un formato enriquecido y uno plano.
- ¿Por qué deja de poder pegarse el contenido si se cierra la aplicación de origen de la copia?
- Porque el origen de la copia usa renderizado diferido (delayed rendering). Las aplicaciones que manejan datos grandes no colocan el cuerpo de los datos en el momento de copiar, sino que registran en el portapapeles solo la promesa de «generarlos cuando se soliciten». Si en este estado el origen se cierra sin responder a WM_RENDERALLFORMATS en el momento del cierre y sin materializar los datos, los formatos no renderizados se pierden. Si la aplicación usa el portapapeles OLE (IDataObject), puede mantener la posibilidad de pegar incluso después de cerrarse llamando a OleFlushClipboard al finalizar para materializar los datos.
- ¿Cómo se puede monitorizar desde una aplicación propia los cambios en el portapapeles?
- El método actualmente recomendado es registrar la propia ventana como listener con AddClipboardFormatListener y procesar el mensaje WM_CLIPBOARDUPDATE que llega cada vez que cambia el contenido. Sondear (polling) el contenido periódicamente con un temporizador es poco eficiente, y la cadena de visores heredada basada en SetClipboardViewer se mantiene solo por compatibilidad hacia atrás, porque un fallo en una sola aplicación de la cadena rompe el conjunto. Además, como el OpenClipboard usado al leer puede fallar por competir con otro proceso, conviene implementar reintentos con una espera breve para que la lectura sea estable.
- ¿Hay alguna forma de evitar que información confidencial, como contraseñas, quede en el historial del portapapeles (Win+V)?
- Existen dos vías, una del lado de la aplicación y otra del lado de las políticas. Del lado de la aplicación, si al copiar se coloca también el formato registrado ExcludeClipboardContentFromMonitorProcessing, ese contenido queda excluido tanto del historial como de la sincronización entre dispositivos. También se puede controlar cada aspecto por separado con CanIncludeInClipboardHistory (solo el historial) y CanUploadToCloudClipboard (solo la sincronización). Este es el mecanismo que usan los gestores de contraseñas. Si se quiere desactivarlo para toda la organización, se puede deshabilitar el propio historial y la sincronización en la nube con AllowClipboardHistory y AllowCrossDeviceClipboard, mediante directiva de grupo o Intune (Policy CSP).
- ¿Por qué no se pueden arrastrar y soltar archivos sobre una aplicación ejecutada como administrador?
- Porque el mecanismo de seguridad UIPI (User Interface Privilege Isolation) bloquea el envío de mensajes desde un proceso de nivel de integridad bajo hacia una ventana de nivel de integridad alto. Como el Explorador de archivos se ejecuta con privilegios normales (integridad media), la notificación de arrastrar y soltar no llega a la ventana de una aplicación elevada. Se conoce una solución alternativa que permite mensajes concretos como WM_DROPFILES de forma individual con ChangeWindowMessageFilterEx, pero se limita a las notificaciones de suelta del estilo antiguo. La solución de fondo es dejar de diseñar la aplicación para que se ejecute siempre elevada y separar en un proceso distinto solo el procesamiento que realmente necesita elevación.
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.