使用 .NET 远程处理共享剪贴板
5.00/5 (17投票s)
2002年10月15日
6分钟阅读
165800
3307
使用 .NET 远程处理将剪贴板内容发送到另一台计算机

引言
你的办公桌上有两台电脑吗?如果有,你是否曾想过在一台电脑上按 Ctrl-C,然后在另一台电脑上按 Ctrl-V?当两台电脑通过 KVM 切换器共享一个键盘和显示器时,这样做尤其诱人。此应用程序使在本地网络上的两台电脑之间共享剪贴板变得容易。该应用程序使用 C# 和 .NET 远程处理编写。本文将解释远程处理的实现和剪贴板处理,并解释如何使用委托来解决线程问题。
使用应用程序
ClipShare 既充当客户端又充当服务器,因此它必须在将共享剪贴板的两台计算机上运行。在具有要共享的剪贴板数据(客户端)的计算机上,指定要发送到的计算机名称,然后选择“发送剪贴板”按钮。剪贴板将被打包,通过局域网发送,并自动放置在接收端(服务器)计算机的剪贴板上。为了方便起见,该应用程序在系统托盘中有一个图标,因此你也可以从图标的上下文菜单中选择“发送剪贴板”。如果你想暂时禁止其他计算机向你的计算机发送剪贴板,请取消选中“允许传入”复选框或上下文菜单项。
设置 .NET 远程处理
此应用程序使用 .NET 远程处理来传输剪贴板内容。首先,我们需要进行一些初始化,使应用程序成为远程处理服务器。此代码片段(来自表单的构造函数)启用远程处理,然后向 RemotingConfiguration 注册一个类 RemoteClipboard(稍后讨论),以便可以远程激活它
TcpChannel channel = new TcpChannel(4820);
hannelServices.RegisterChannel(channel);
RemotingConfiguration.RegisterWellKnownServiceType(typeof(RemoteClipboard),
"ClipShare",
WellKnownObjectMode.Singleton);
此应用程序还充当远程处理客户端。要发送剪贴板数据,客户端使用上面注册的相同端口号和服务名在服务器上激活 RemoteClipboard 类的一个实例。我们将使用此实例调用 SendClipboard 方法,因此我们将其保存在一个成员变量中供以后使用
private void InitRemoteObject()
{
try
{
string location = "tcp://" + computerName.Text + ":4820/ClipShare";
m_remoteClipboard = (RemoteClipboard) Activator.GetObject(
typeof(RemoteClipboard), location);
computerName.Modified = false;
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
RemoteClipboard 类
RemoteClipboard 类被远程激活,用于将剪贴板内容从一台计算机传输到另一台计算机。它必须继承自 MarshalByRefObject,以便可以远程激活。它有一个方法 SendClipboard,它接受一个包含从客户端传入的剪贴板数据的 ArrayList,并将其放置在服务器的剪贴板上。
刚开始就遇到的问题
不幸的是,SendClipboard 方法不允许将数据放置在剪贴板上。用于设置剪贴板内容的 .NET Framework 方法 Clipboard.SetDataObject() 只能在单线程单元 (STA) 中运行,但是由于 RemoteClipboard 对象是远程激活的,它在多线程单元 (MTA) 中运行。如果你尝试直接从 SendClipboard 调用 Clipboard.SetDataObject(),则会抛出异常。
委托来救援
为了解决这个问题,我们需要让表单将数据放置在剪贴板上,而不是 RemoteClipboard 对象。由于 Form.Main() 声明了 [STAThread] 属性,它可以毫无问题地调用 SetDataObject。为了让表单在其自己的线程中处理数据,我们需要在表单上调用 Invoke 方法,传入一个指向表单方法之一 (AddToClip) 的委托。RemoteClipboard 类有一个 static 方法 SetOnClipReceive,在初始化期间调用一次,以便在调用 Invoke 时为其提供表单和要使用的委托。下面是一个图,有助于概念性地描述正在发生的事情,随后是完整的 RemoteClipboard 类

public delegate void ClipEventHandler(ArrayList clipData);
public class RemoteClipboard : MarshalByRefObject
{
private static ClipEventHandler m_OnClipReceive;
private static Form m_receiverForm;
// set the object and method that gets a callback when a clipboard is sent.
public static void SetOnClipReceive(Form receiver, ClipEventHandler theCallback)
{
m_receiverForm = receiver;
m_OnClipReceive = theCallback;
}
// this is the method that will be invoked remotely by the other computer.
public void SendClipboard(ArrayList clipData)
{
object[] clipObjects = {clipData};
m_receiverForm.Invoke(m_OnClipReceive, clipObjects);
}
}
SetOnClipReceive 函数从表单的构造函数中调用
RemoteClipboard.SetOnClipReceive(this, new ClipEventHandler(this.AddToClip));
给 Forms 开发人员的特别提示:我将 RemoteClipboard 类包含在与表单相同的源文件中,因为这样做很小巧方便。我最初将 RemoteClipboard 类定义在源文件中的表单上方。我这样做后(尽管我从未建立这种联系),表单无法再访问其资源,例如系统托盘的图标。我花了一段时间才弄清楚 Form 必须首先在源文件中定义。根据 Microsoft 的说法,这是设计使然(请参阅 Q318603)。
打包剪贴板内容
要访问剪贴板数据,请使用 Clipboard.GetDataObject(),它返回 IDataObject 接口的一个实例。不幸的是,此对象不可序列化,因此无法作为参数传递给 SendClipboard 方法。相反,我们遍历剪贴板上的每种格式,如果它是可序列化的,则将剪贴板数据项放入一个数组列表中,并与它的格式字符串配对。然后使用 SendClipboard 方法将 ArrayList 传递给 RemoteClipboard 对象。
private void SendClipboardToRemote()
{
try
{
...
ArrayList dataObjects = new ArrayList();
IDataObject clipboardData = Clipboard.GetDataObject();
string[] formats = clipboardData.GetFormats();
for (int i=0; i < formats.Length; i++)
{
object clipboardItem = clipboardData.GetData(formats[i]);
if (clipboardItem != null && clipboardItem.GetType().IsSerializable)
{
Console.WriteLine("sending {0}", formats[i]);
dataObjects.Add(formats[i]);
dataObjects.Add(clipboardItem);
}
else
Console.WriteLine("ignoring {0}", formats[i]);
}
if (dataObjects.Count > 0)
{
Cursor.Current = Cursors.WaitCursor;
m_remoteClipboard.SendClipboard(dataObjects);
Cursor.Current = Cursors.Default;
}
else
MessageBox.Show(this, "Nothing on clipboard, or contents not supported",
"ClipShare");
}
catch (Exception ex)
{
string message = String.Format("Unable to send data: {0}", ex.Message);
MessageBox.Show(this, message, "ClipShare");
}
}
接收剪贴板
在接收端,我们遍历数组列表并将每个剪贴板数据项添加到新的 DataObject 中,然后通过 Clipboard.SetDataObject 将其放置在剪贴板上。此处显示的 AddToClip 方法是被 SendClipboard 方法调用的委托(见上文)
public void AddToClip(ArrayList theData)
{
if (!allowIncomingCB.Checked)
throw new Exception("Remote computer has disabled clipboard sharing");
try
{
DataObject dataObj = new DataObject();
for (int i = 0; i < theData.Count; i++)
{
string format = (string)theData[i++];
dataObj.SetData(format, theData[i]);
}
Clipboard.SetDataObject(dataObj, true);
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
你可能已经注意到,客户端对 SendClipboard 的调用是在 try/catch 块中,因此在服务器上的 AddToClip 中抛出的异常(“远程计算机已禁用剪贴板共享”)将传播回客户端并在消息框中显示。同样值得注意的是,但并不奇怪的是,如果使用 BeginInvoke 而不是 Invoke 异步调用 AddToClip,则异常将不会传播,并且你将收到未处理的异常错误。
限制
不幸的是,从 IDataObject 检索到的并非所有格式都可序列化。例如,Windows 图元文件格式就不是,因此与绘图程序之间的传输仅限于位图格式。此外,如果你复制文件或目录,放置在剪贴板上的位置使用驱动器盘符而不是 UNC,因此它们无法粘贴到远程计算机上。我想在发送剪贴板之前添加预处理以将路径更改为使用 UNC 并不难。最后,我半心半意地尝试让系统托盘图标上的左键单击除了右键单击之外也显示上下文菜单(如果你下载它,这在源文件中被注释掉了)。这似乎不是 NotifyIcon 类直接支持的,因此不容易实现。
结论
当我开始编写这个应用程序时,我以为它会是一个快速的远程处理入门,但通常情况下,尤其是在熟悉一个新的编程环境时,它比我预期的要花费更多时间。但是遇到问题并不全是坏事,因为解决它们是学习过程的一部分。(下次,我知道不要将类定义在我的表单上方了!)
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。
