Brian Trautman
¿Ha trabajado alguna vez en varios equipos y ha deseado poder copiar el contenido del portapapeles de un equipo a otro? A menudo he pensado que sería perfecto si existiera una forma rápida y sencilla de mover fragmentos de texto, capturas de pantalla o incluso archivos a otro equipo simplemente con copiar y pegar. Si le parece interesante, siga leyendo.
Quería que esto funcionara independientemente de si los equipos estaban en línea al mismo tiempo o no y no quería que estuvieran interrumpidos por firewalls, NAT's, etc. así que, opté por una arquitectura basada en servidor en lugar de una arquitectura punto a punto. La arquitectura implicaba que una aplicación cliente transfiriera el contenido del portapapeles al servidor mediante una llamada de servicios web, un servicio web para guardar en caché el contenido del portapapeles; y que otro componente cliente recuperara el contenido del portapapeles del servidor y lo colocara en el portapapeles del equipo local.
Para solucionar este problema, necesitamos el acceso mediante programación al portapapeles tanto en el equipo desde el que se copia como en el equipo al que se copia. Afortunadamente, .NET ofrece un contenedor administrado alrededor de la API nativa del Portapapeles de Windows que nos da acceso a la misma. Los espacios de nombres pertinentes son Clipboard for C# y my.Computer.Clipboard. Como estamos interesados en mover objetos del portapapeles de un equipo a otro, primero necesitamos determinar qué tipo de objetos correspondientes a nuestras acciones se colocan en el portapapeles (copiar texto, imágenes y archivos). La escritura de un fragmento de código rápido con el espacio de nombres Clipboard nos permite repetir con todos los objetos en el portapapeles para varios tipos de acciones para ver con qué estamos trabajando.
| Contenido |
Visual C#
IDataObject clipData = Clipboard.GetDataObject();
//retrieve an array of strings for all the formats available on the clipboard.
string[] formats = clipData.GetFormats();
//iterate through the list of formats on the clipboard
foreach (string format in formats)
{
//add each objeoct to an arraylist so we can inspect the object types
object dataObject = clipData.GetData(format);
}
Visual Basic
Dim clipData As IDataObject = Clipboard.GetDataObject
Dim formats() As String = clipData.GetFormats
'iterate through the list of formats on the clipboard
For Each format As String In formats
'add each objeoct to an arraylist so we can inspect the object types
Dim dataObject As Object = clipData.GetData(format)
The screenshot below shows the results for having some text from Word copied onto the clipboard. Each format in the string array is a representation of the data on the clipboard. Since the target application (where you’re going to paste to) is unknown the clipboard has multiple formats each containing the same data. The objective with this project is to replicate all these formats on the target machine’s clipboard

Después de la inspección de los objetos del portapapeles que corresponden a objetos de texto, imágenes y archivos, era fácil determinar los tipos de objetos primarios sobre los que debemos ocuparnos. Estos son "System.IO.MemoryStream", "System.IO.FileStream", "System.Drawing.Bitmap", y "System.String". Puesto que toda esta información se transferiría al servidor vía servicios web, es sencillo serializar todos objetos a bytes para la transmisión. Esto es necesario por varias razones, inclusive el hecho de que los objetos complejos tales como MemoryStreams no pueden ser simplemente serializados y enviados a través de la llamada a servicio web como Strings. Además, algunos de los objetos son más grandes de lo que permite el servicio web y necesitarían ser divididos en partes más pequeñas para la transmisión y después volverse a unir en el lado de servidor en el orden correcto. Otra vez, cuando el cliente solicita los elementos del portapapeles, necesitaremos desensamblar cada objeto, enviarlo a través del resultado de retorno del servicio web al cliente, y entonces volver a unirlo.
El primer elemento que se debe construir es una función que realice la división de estas corrientes grandes en matrices de bytes más manejables para la transmisión al servicio web. La función de abajo realiza esta tarea enviando grupos del MemoryStream con un tamaño limitado por la constante “byteCount”. Una vez que este límite es alcanzado, el búfer es mandado a través de la llamada a servicio web para el almacenamiento y la unión en el servidor. Una vez que nos quedan 0 bytes que enviar o un número inferior a “byteCount”, enviamos los elementos restantes del búfer e indicamos al servicio web que este objeto en concreto está completo con el marcador “isFinalTransaction”.
Visual C#
private void UploadStreamBlock(string format, string objectType,
MemoryStream memStream)
{
//each time we enter this function we have a new transaction beginning. A
transaction represents a comlete
//object on the clipboard and we'll use this on the server side to know how to
put the stream back together
string transactionGuid = System.Guid.NewGuid().ToString();
memStream.Position = 0;
byte[] buffer = new byte[byteCount];
bool isFinalTransaction = false;
//while the current stream position plus our byte count is less than the length
of the stream continue sending as much
//as we can.
while ((memStream.Position + byteCount) <= memStream.Length)
{
//if we happen to be on the last byte of the stream set the final transaction
flag to true so the server
//will know that this is the last bit of this transaction to expect.
if (memStream.Position + byteCount == memStream.Length)
{
isFinalTransaction = true;
}
//read the stream into our buffer for transmission over the web service.
memStream.Read(buffer, 0, byteCount);
ws.InsertMessageStream(buffer, format, objectType, transactionGuid,
isFinalTransaction, clipBoardGUID);
}
long remainingBytes = memStream.Length - memStream.Position;
//if we still have remaining bytes left figure out how many and transmit the
last bit of this ojbect over the
//web service.
if ((int)remainingBytes > 0)
{
byte[] remainingBuffer = new byte[(int)remainingBytes];
memStream.Read(remainingBuffer, 0, (int)remainingBytes);
ws.InsertMessageStream(remainingBuffer, format, objectType,
transactionGuid, true, clipBoardGUID);
}
}
Visual Basic
Private Sub UploadStreamBlock(ByVal format As String, ByVal objectType
As String, ByVal memStream As MemoryStream)
'each time we enter this function we have a new transaction beginning. A
transaction represents a comlete
'object on the clipboard and we'll use this on the server side to know how to
put the stream back together
Dim transactionGuid As String = System.Guid.NewGuid.ToString
memStream.Position = 0
Dim buffer() As Byte = New Byte((byteCount) - 1) {}
Dim isFinalTransaction As Boolean = False
'while the current stream position plus our byte count is less than the length of
the stream continue sending as much
'as we can.
While ((memStream.Position + byteCount) _
<= memStream.Length)
'if we happen to be on the last byte of the stream set the final transaction
flag to true so the server
'will know that this is the last bit of this transaction to expect.
If ((memStream.Position + byteCount) _
= memStream.Length) Then
isFinalTransaction = True
End If
'ream the stream into our buffer for transmission over the web service.
memStream.Read(buffer, 0, byteCount)
clipService.InsertMessageStream(buffer, format, objectType,
transactionGuid, isFinalTransaction, clipBoardGUID)
End While
Dim remainingBytes As Long = (memStream.Length - memStream.Position)
'if we still have remaining bytes left figure out how many and transmit the last
bit of this ojbect over the
'web service.
If (CType(remainingBytes, Integer) > 0) Then
Dim remainingBuffer() As Byte = New Byte((CType(remainingBytes, Integer))
- 1) {}
memStream.Read(remainingBuffer, 0, CType(remainingBytes, Integer))
clipService.InsertMessageStream(remainingBuffer, format, objectType,
transactionGuid, True, clipBoardGUID)
End If
End Sub
El lado de servidor del servicio web necesita volver a unir todo el portapapeles desde varias matrices de bytes, por lo que es importante que todos los objetos, sus tipos y los formatos se conserven para que el portapapeles funcione correctamente en el equipo de destino. Usamos el clipBoardGuid para determinar si estamos en una nueva publicación de portapapeles o agregando objetos a una instancia ya existente. Usamos el marcador isFinalTranaction para saber si esta matriz de bytes debe formar parte de una transacción existente o es la primera en una transacción nueva. Todos los elementos del portapapeles se guardan en el disco para la recuperación posterior por parte de cualquier cliente que los solicita. El código para esto está abajo.
Visual C#
[WebMethod]
public void InsertMessageStream(byte[] buffer, string format, string objectType,
string transactionGuid, bool isFinalTransaction, string clipBoardGUID)
{
//always base the current directory on the clipboard that we're sending now.
string clipBoardGUIDDirectory = System.Web.HttpContext.Current.Request.
PhysicalApplicationPath + clipBoardGUID;
try
{
//if the directory does not exist then delete all the other directories (clipboard
instances) and create a new directory
//if the directory already exists then this particular transaction is part of the
same clipboard so don't do anything.
//this works because othe clipboardDirectory is based off of the GUID sent
from the client.
if (!Directory.Exists(clipBoardGUIDDirectory))
{
string[] dirs = Directory.GetDirectories(System.Web.HttpContext.
Current.Request.PhysicalApplicationPath);
foreach (string dir in dirs)
{
Directory.Delete(dir, true);
}
Directory.CreateDirectory(clipBoardGUIDDirectory);
}
}
catch
{
}
//create the filename based on the current transaction, format, and object type.
We will parse this out later
//so we know how to add this back to the target clipboard.
string fileName = clipBoardGUIDDirectory + "\\" + transactionGuid + "_" +
format + "_" + objectType;
FileStream fs = new FileStream(fileName, FileMode.Append,
FileAccess.Write);
fs.Position = fs.Length;
fs.Write(buffer, 0, buffer.Length);
fs.Close();
}
Visual Basic
<WebMethod()> _
Public Sub InsertMessageStream(ByVal buffer() As Byte, ByVal format As
String, ByVal objectType As String, ByVal transactionGuid As String, ByVal
isFinalTransaction As Boolean, ByVal clipBoardGUID As String)
'always base the current directory on the clipboard that we're sending now.
Dim clipBoardDataDirectory As String = (System.Web.HttpContext.Current.Request.
PhysicalApplicationPath + "\\Clipboard_Data")
Dim clipBoardGUIDDirectory As String = (clipBoardDataDirectory + ("\\" +
clipBoardGUID))
Try
'if the directory does not exist then delete all the other directories (clipboard
instances) and create a new directory
'if the directory already exists then this particular transaction is part of the
same clipboard so don't do anything.
'this works because othe clipboardDirectory is based off of the GUID sent
from the client.
If Not Directory.Exists(clipBoardGUIDDirectory) Then
Dim dirs() As String = Directory.GetDirectories(clipBoardDataDirectory)
For Each dir As String In dirs
Directory.Delete(dir, True)
Next
Directory.CreateDirectory(clipBoardGUIDDirectory)
End If
Catch
End Try
'create the filename based on the current transaction, format, and object type.
We will parse this out later
'so we know how to add this back to the target clipboard.
Dim fileName As String = (clipBoardGUIDDirectory + ("\\" _
+ (transactionGuid + ("_" _
+ (format + ("_" + objectType))))))
Dim fs As FileStream = New FileStream(fileName, FileMode.Append,
FileAccess.Write)
fs.Position = fs.Length
fs.Write(buffer, 0, buffer.Length)
fs.Close()
End Sub
Cada objeto del formato del portapapeles se almacena en disco para la recuperación posterior por parte del cliente. Observe en el screenshot de abajo cómo el nombre del archivo se usa para almacenar el único transactionID correspondiente al objeto, el tipo de objeto y también el formato del portapapeles. Todos estos datos son necesarios para volver a unir los elementos correctamente y colocarlos en el portapapeles de destino.

Ahora que tenemos una representación de cada objeto de formato del portapapeles en el servidor, necesitamos una manera de volver a conseguir cada elemento en el portapapeles de destino. El método siguiente del servicio web ofrece un resultado de retorno del tipo “ClipboardStream”. El objeto ClipboardStream contiene toda información pertinente necesaria para volver a unir cada elemento en el portapapeles de destino. Puesto que un servicio web es un tipo de solicitud-respuesta, el servicio web espera que el cliente continúe llamando al servicio web hasta que los todos los elementos del portapapeles se hayan recibido correctamente. Además, se introduce una complejidad adicional porque cada elemento individual del portapapeles se puede dividir en varios elementos si exceden la longitud máxima establecida por nuestra constante “byteCount”; por lo tanto, el equipo de destino debe realizar un seguimiento de cada solicitud e indicar al servidor dónde se quedó la última transacción vía la variable denominada “currentByte”. El código del servicio web se muestra más abajo.
Visual C#
[WebMethod]
public ClipboardStream GetMessageStream(string transactionGUID, string[]
previousTransactionGUIDs, string clipBoardGUID, long currentByte)
{
string clipBoardDataDirectory = System.Web.HttpContext.Current.Request.
PhysicalApplicationPath + "Clipboard_Data";
string clipBoardGUIDDirectory = clipBoardDataDirectory + "\\" +
clipBoardGUID;
string currentTransaction = ";
bool isLastTransaction = false;
//if the clipBoardGUID is not empty then we only need to make sure that the
directory still exists.
if (clipBoardGUID != ")
{
//if the directory does not exist throw an exception, it must have already
been deleted.
if (!Directory.Exists(clipBoardGUIDDirectory))
{
throw new Exception("Requested clipboard does not exist. It must have
been deleted.");
}
}
//if the clipboardGUID is empty then this is the client's first contact with the
server and we need
//to select the available clipboard GUID to return to the user.
else
{
string[] availableClipBoard =
Directory.GetDirectories(clipBoardDataDirectory)[0].Split('\\');
clipBoardGUID = availableClipBoard[availableClipBoard.Length - 1];
clipBoardGUIDDirectory += clipBoardGUID;
}
//we need to get the next transaction. Each time we finish a transaction we
add it to previousTransactionGUIDs
//at the client end so we know not to send it again.
currentTransaction = GetCurrentTransaction(clipBoardGUIDDirectory,
previousTransactionGUIDs);
//if the current transaction is null then we're done and there are no more to send to the client
if (currentTransaction == null)
{
return null;
}
//open the filestream and set it to the position requested by the client.
FileStream fs = new FileStream(currentTransaction, FileMode.Open);
fs.Position = currentByte;
//determind if this is the last transaction or not for this object so we can let the
client know.
long numBytesToRead = fs.Length - currentByte;
if (numBytesToRead > byteCount)
{
numBytesToRead = byteCount;
isLastTransaction = false;
}
else
{
isLastTransaction = true;
}
//read the filestream bytes to the buffer and populate the object to return to the
client.
byte[] buffer = new byte[numBytesToRead];
fs.Read(buffer, 0, (int)numBytesToRead);
fs.Close();
FileInfo fi = new FileInfo(currentTransaction);
ClipboardStream clipboardStream = new ClipboardStream();
clipboardStream.Buffer = buffer;
clipboardStream.ClipBoardID = clipBoardGUID;
clipboardStream.Format = fi.Name.Split('_')[1];
clipboardStream.ObjectType = fi.Name.Split('_')[2];
clipboardStream.IsLastTransaction = isLastTransaction;
clipboardStream.TransactionID = currentTransaction;
return clipboardStream;
}
Visual Basic
<WebMethod()> _
Public Function GetMessageStream(ByVal transactionGUID As String, ByVal
previousTransactionGUIDs() As String, ByVal clipBoardGUID As String, ByVal
currentByte As Long) As ClipboardStream
Dim clipBoardDataDirectory As String =
(System.Web.HttpContext.Current.Request.PhysicalApplicationPath +
"Clipboard_Data")
Dim clipBoardGUIDDirectory As String = clipBoardDataDirectory
Dim currentTransaction As String = "
Dim isLastTransaction As Boolean = False
'if the clipBoardGUID is not empty then we only need to make sure that the
directory still exists.
If (clipBoardGUID <> ") Then
¿Ha trabajado alguna vez en varios equipos y ha deseado poder copiar el contenido del portapapeles de un equipo a otro? A menudo he pensado que sería perfecto si existiera una forma rápida y sencilla de mover fragmentos de texto, capturas de pantalla o incluso archivos a otro equipo simplemente con copiar y pegar. En este proyecto, exploramos cómo transferir el contenido del portapapeles de un equipo a otro con un servicio web.