http://www.microsoft.com/china/MSDN/library/WebServices/WebServices/WebServices.mspx?mfr=true
您曾經在多台電腦上工作過嗎?您希望能夠將剪貼簿內容從一台電腦複製到其他電腦嗎?我一直都很希望能夠有一種快速而簡便的方法,通過簡單的複製和粘貼將文本程式碼片段、螢幕快照、甚至檔案移動到其他電腦上。如果您對這個話題感興趣,那麼請繼續閱讀本文。
我希望無論兩台電腦是否同時線上都可以實現此操作,並且不會被防火牆、NAT 等停止。因此我選擇基於伺服器的體繫結構而非對等體繫結構。該體繫結構包括用戶端應用程式(通過調用 Web 服務將剪貼簿內容傳輸到伺服器)、Web 服務(緩衝剪貼簿內容)、另一個用戶端組件(從伺服器上檢索剪貼簿內容並將它們放置到本機電腦的剪貼簿上)。
為解決這個問題,我們需要以編程方式在作為複製來源和複製目標的兩台電腦上訪問剪貼簿。值得慶幸的是,.NET 在本地 Windows 剪貼簿 API 中提供了一個受管封裝程式,我們可以通過它進行訪問。相關命名空間是 C# 的 Clipboard 和 my.Computer.Clipboard。由於我們所感興趣的是將剪貼簿對象從一台電腦移動到另一台,因此我們首先需要確定要將哪些類型的對象放置到剪貼簿上,以便於我們對其進行各種操作(複製文本、映像及檔案)。通過使用 Clipboard 命名空間編寫簡要程式碼片段,我們可以遍曆要對其執行各種類型操作的剪貼簿上的所有對象,以瞭解我們正在處理的內容。
Visual C#
IDataObject clipData = Clipboard.GetDataObject();//檢索剪貼簿上所有可用格式的一個字串數組。string[] formats = clipData.GetFormats();//遍曆剪貼簿可用格式列表foreach (string format in formats){//將每個對象添加到一個數組列表,以便我們能夠檢查物件類型object dataObject = clipData.GetData(format);}
Visual Basic
Dim clipData As IDataObject = Clipboard.GetDataObjectDim formats() As String = clipData.GetFormats'遍曆剪貼簿可用格式列表For Each format As String In formats'將每個對象添加到一個數組列表,以便我們能夠檢查物件類型Dim dataObject As Object = clipData.GetData(format)Next
下面的螢幕快照顯示了將 Word 中的文本複製到剪貼簿中的結果。字串數組中的每一個格式都表示剪貼簿中的資料。因為目標應用程式(我們將向其中粘貼)還是未知的,所以剪貼簿中有多種格式,每種格式包含相同的資料。本項目的目的是在目標電腦的剪貼簿上複製所有這些格式。
檢查完剪貼簿上的文本、映像及檔案對象的對象之後,就很容易確定出我們需要關注的主要物件類型。它們是“System.IO.MemoryStream”、“System.IO.FileStream”、“System.Drawing.Bitmap”和“System.String”。因為所有這些資訊都會通過 Web 服務傳輸到伺服器,一種簡單的方法就是將所有對象序列化為位元組進行傳輸。這樣操作的原因有很多,其中一項事實就是,複雜物件(例如 MemoryStream)不能像 Strings 那樣被簡單地序列化並通過 Web 服務發送。此外,某些對象很大,已超出 Web 服務調用所允許的範圍,因此在傳輸時需要分解成較小的部分,然後再在伺服器端以正確的順序重新組裝。同樣,當用戶端請求剪貼簿項目時,我們需要分解各個對象,然後通過 Web 服務將結果返回到用戶端,接著再重新組裝。
要建立的第一項是一個基本函數,它將這些很大的流分解成更多的易於管理的位元組數組,以傳輸給 Web 服務。下面的這個函數通過發送 MemoryStream 塊執行該任務,其中的塊大小通過“byteCount”常量進行限制。達到該限制值之後,緩衝區中的內容就會通過調用 Web 服務來發送,以在伺服器上進行儲存和組裝。一旦我們需要發送的內容為 0 位元組或位元組數少於“byteCount”常量數,我們將發送緩衝區中的剩餘元素,並使用“isFinalTransaction”標誌來通知 Web 服務此特定對象已傳輸完畢。
Visual C#
private void UploadStreamBlock(string format, string objectType, MemoryStream memStream){//每次我們輸入此函數,即開始了一個新事務。一個事務代表剪貼簿上的一個完整對象,//我們在伺服器端使用它來知道如何將流放回到一起string transactionGuid = System.Guid.NewGuid().ToString();memStream.Position = 0;byte[] buffer = new byte[byteCount];bool isFinalTransaction = false;//當目前的流位置加上我們的位元組計數小於流長度時,儘可能//繼續發送。while ((memStream.Position + byteCount) <= memStream.Length){//如果恰好位於流的最後一個位元組,則將最終事務標誌設定為 true,使伺服器//知道這是所需的此事務的最後一位。if (memStream.Position + byteCount == memStream.Length){isFinalTransaction = true;}//將流讀入緩衝區,以便通過 Web 服務進行傳輸。memStream.Read(buffer, 0, byteCount);ws.InsertMessageStream(buffer, format, objectType, transactionGuid, isFinalTransaction, clipBoardGUID);}long remainingBytes = memStream.Length - memStream.Position;//如果還有剩餘位元組,則計算出還有多少剩餘位元組並通過 Web 服務傳輸此對象的//最後一位。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)'每次我們輸入此函數,即開始了一個新事務。一個事務代表剪貼簿上的一個完整對象,'我們在伺服器端使用它來知道如何將流放回到一起Dim transactionGuid As String = System.Guid.NewGuid.ToStringmemStream.Position = 0Dim buffer() As Byte = New Byte((byteCount) - 1) {}Dim isFinalTransaction As Boolean = False'當目前的流位置加上我們的位元組計數小於流長度時,儘可能'繼續發送。While ((memStream.Position + byteCount) _<= memStream.Length)'如果恰好位於流的最後一個位元組,則將最終事務標誌設定為 true,使伺服器'知道這是所需的此事務的最後一位。If ((memStream.Position + byteCount) _= memStream.Length) ThenisFinalTransaction = TrueEnd If'將流讀入緩衝區,以便通過 Web 服務進行傳輸。memStream.Read(buffer, 0, byteCount)clipService.InsertMessageStream(buffer, format, objectType, transactionGuid, isFinalTransaction, clipBoardGUID)End WhileDim remainingBytes As Long = (memStream.Length - memStream.Position)'如果還有剩餘位元組,則計算出還有多少剩餘位元組並通過 Web 服務傳輸此對象的'最後一位。If (CType(remainingBytes, Integer) > 0) ThenDim 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 IfEnd Sub
Web 服務的伺服器端需要將全部剪貼簿內容由大量的位元組數組重新組合到一起,因此保留所有對象、對象的類型以及格式對於剪貼簿在目標電腦上正常工作是至關重要的。我們使用 clipBoardGuid 來確定我們是在進行新的剪貼簿張貼,還是在將對象添加到已有執行個體上。同時使用 isFinalTranaction 標誌來瞭解此位元組數組應該是現有事務的一部分,還是新事務中的第一個。所有剪貼簿項都會儲存到磁碟中,以便稍後由請求它們的任何用戶端進行檢索。下面是執行此功能的代碼。
Visual C#
[WebMethod]public void InsertMessageStream(byte[] buffer, string format, string objectType, string transactionGuid, bool isFinalTransaction, string clipBoardGUID){//目前的目錄始終基於此時正發送的剪貼簿。string clipBoardGUIDDirectory = System.Web.HttpContext.Current.Request.PhysicalApplicationPath + clipBoardGUID;try{//如果該目錄不存在,則刪除所有其他目錄(剪貼簿執行個體)並建立一個新目錄//如果該目錄已經存在,則此特定事務是同一剪貼簿的一部分,因此不要做任何事情。//這能奏效是因為 clipboardDirectory 不是從用戶端發送的 GUID。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{}//根據當前事務、格式和物件類型建立檔案名稱。我們將在以後對此進行解析,//以便知道如何將其添加回目標剪貼簿。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)'目前的目錄始終基於此時正發送的剪貼簿。Dim clipBoardDataDirectory As String = (System.Web.HttpContext.Current.Request.PhysicalApplicationPath + "\\Clipboard_Data")Dim clipBoardGUIDDirectory As String = (clipBoardDataDirectory + ("\\" + clipBoardGUID))Try'如果該目錄不存在,則刪除所有其他目錄(剪貼簿執行個體)並建立一個新目錄'如果該目錄已經存在,則此特定事務是同一剪貼簿的一部分,因此不要做任何事情。'這能奏效是因為 clipboardDirectory 不是基於從用戶端發送的 GUID。If Not Directory.Exists(clipBoardGUIDDirectory) ThenDim dirs() As String = Directory.GetDirectories(clipBoardDataDirectory)For Each dir As String In dirsDirectory.Delete(dir, True)NextDirectory.CreateDirectory(clipBoardGUIDDirectory)End IfCatchEnd Try'根據當前事務、格式和物件類型建立檔案名稱。我們將在以後對此進行解析'以便知道如何將其添加回目標剪貼簿。Dim fileName As String = (clipBoardGUIDDirectory + ("\\" _+ (transactionGuid + ("_" _+ (format + ("_" + objectType))))))Dim fs As FileStream = New FileStream(fileName, FileMode.Append, FileAccess.Write)fs.Position = fs.Lengthfs.Write(buffer, 0, buffer.Length)fs.Close()End Sub
每種剪貼簿格式對象都儲存在磁碟上,以便用戶端以後進行檢索。請注意下面螢幕快照中如何使用檔案名稱來儲存物件、物件類型以及剪貼簿格式的唯一 transactionID。所有這些資訊片段對於正確地重新組裝項以及將它們放置到目標剪貼簿上都是必不可少的。
現在伺服器上對每種剪貼簿格式對象都有了對應的表示,我們需要一種方法能夠將每一項重新放回目標剪貼簿上。下面的 Web 服務方法提供類型為“ClipboardStream”的返回結果。ClipboardStream 對象包含將各項重新組裝到目標剪貼簿中所必需的所有相關資訊。因為 Web 服務是要求-回應類型關係,所以 Web 服務期望用戶端繼續調用 Web 服務,直到成功接收所有剪貼簿項。此外,更大的複雜性也由此引入,因為每個單獨的剪貼簿項都可能會拆分成多個項(當它們超出常量“byteCount”所設定的最大長度時),因此目標電腦必須跟蹤每個請求並通過名為“currentByte”的變數告知伺服器最後一個事務停止的位置。Web 服務代碼如下所示。
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;//如果 clipBoardGUID 不為空白,則只需確保該目錄仍存在。if (clipBoardGUID != ""){//如果該目錄不存在,會引發異常,它一定已經被刪除了。if (!Directory.Exists(clipBoardGUIDDirectory)){throw new Exception("請求的剪貼簿不存在。它一定已經被刪除了。");}}//如果 clipboardGUID 為空白,則這是用戶端與伺服器的第一次接觸,我們需要//選擇可用的剪貼簿 GUID 返回給使用者。else{string[] availableClipBoard = Directory.GetDirectories(clipBoardDataDirectory)[0].Split('\\');clipBoardGUID = availableClipBoard[availableClipBoard.Length - 1];clipBoardGUIDDirectory += clipBoardGUID;}//我們需要擷取下一個事務。每次完成一個事務,我們都在用戶端將其添加到 previousTransactionGUIDs,//使我們知道不用再次發送它。currentTransaction = GetCurrentTransaction(clipBoardGUIDDirectory, previousTransactionGUIDs);//如果當前事務為空白,則我們的工作已經完成,已經沒有內容需要發送給用戶端if (currentTransaction == null){return null;}//開啟檔案流並將其設定到用戶端需要的位置。FileStream fs = new FileStream(currentTransaction, FileMode.Open);fs.Position = currentByte;//確定這是否是該對象的最後一個事務,以便通知用戶端。long numBytesToRead = fs.Length - currentByte;if (numBytesToRead > byteCount){numBytesToRead = byteCount;isLastTransaction = false;}else{isLastTransaction = true;}//將檔案流位元組讀入緩衝區並填充對象以返回給用戶端。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 ClipboardStreamDim clipBoardDataDirectory As String = (System.Web.HttpContext.Current.Request.PhysicalApplicationPath + "Clipboard_Data")Dim clipBoardGUIDDirectory As String = clipBoardDataDirectoryDim 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'if the directory does not exist throw an exception, it must have already been deleted.If Not Directory.Exists(clipBoardGUIDDirectory) ThenThrow New Exception("Requested clipboard does not exist. It must have been deleted.")End IfEnd If'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.Dim availableClipBoard() As String = Directory.GetDirectories(clipBoardDataDirectory)(0).Split(Microsoft.VisualBasic.ChrW(92))clipBoardGUID = availableClipBoard((availableClipBoard.Length - 1))clipBoardGUIDDirectory = (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 clientIf (currentTransaction Is Nothing) ThenReturn NothingEnd If'open the filestream and set it to the position requested by the client.Dim fs As FileStream = 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.Dim numBytesToRead As Long = (fs.Length - currentByte)If (numBytesToRead > byteCount) ThennumBytesToRead = byteCountisLastTransaction = FalseElseisLastTransaction = TrueEnd If'read the filestream bytes to the buffer and populate the object to return to the client.Dim buffer() As Byte = New Byte((numBytesToRead) - 1) {}fs.Read(buffer, 0, CType(numBytesToRead, Integer))fs.Close()Dim fi As FileInfo = New FileInfo(currentTransaction)Dim clipboardStream As ClipboardStream = New ClipboardStreamclipboardStream.Buffer = bufferclipboardStream.ClipBoardID = clipBoardGUIDclipboardStream.Format = fi.Name.Split(Microsoft.VisualBasic.ChrW(95))(1)clipboardStream.ObjectType = fi.Name.Split(Microsoft.VisualBasic.ChrW(95))(2)clipboardStream.IsLastTransaction = isLastTransactionclipboardStream.TransactionID = currentTransactionReturn clipboardStreamEnd Function