从.NET设置剪贴板时出现CLIPBRD_E_CANT_OPEN错误

62

以下代码为什么会有时出现“CLIPBRD_E_CANT_OPEN”异常:

Clipboard.SetText(str);

这通常发生在应用程序第一次使用剪贴板时,而不是之后。


这是一个相当笨拙的解决方案 - 这真的是唯一的方法吗? - Blorgbeard
这似乎是MS在Forms中实现它的方式。虽然我没有意识到它很重要,但这个问题是关于WPF的。 - Robert Wagner
10个回答

48

这是由终端服务剪贴板(以及可能的其他因素)和剪贴板的.NET实现中的一个错误/特性引起的。打开剪贴板的延迟会导致错误,通常在几毫秒内就会消失。

解决方法是在循环内尝试多次,并在它们之间睡眠。

for (int i = 0; i < 10; i++)
{
    try
    {
        Clipboard.SetText(str);
        return;
    }
    catch { }
    System.Threading.Thread.Sleep(10);
} 

4
如果你查看Clipboard.SetText的内部实现,至少在.NET 2.0 SP1版本中,你会发现它已经有了一个重试/等待循环。最多重试10次,每次延迟100毫秒。 - Mike Dimmick
18
@Mike:System.Windows.Forms.Clipboard 拥有重试功能,但 WPF 中的 System.Windows.Clipboard 则没有。 - Cameron MacFarland
7
这太疯狂了,纯粹的疯狂... :D 我花了2个小时没有查阅任何资料试图弄清楚如何让这可恶的东西工作,而你告诉我应该一直尝试在一个可恶的循环中直到它工作?疯狂! :) - bor
13
catch {} 是一种不好的做法。应该用 catch (COMException ex) { const uint CLIPBRD_E_CANT_OPEN = 0x800401D0; if ((uint)ex.ErrorCode != CLIPBRD_E_CANT_OPEN) throw; } 替代。 - Maxence
实际上,WinForms的剪贴板具有重试功能,可以在此处查看:https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/Clipboard.cs,171 - Borislav Ivanov

43

实际上,我认为这是Win32 API的错误

要设置剪贴板中的数据,您必须先打开它。一次只能有一个进程打开剪贴板。因此,当您检查时,如果另一个进程出于任何原因打开了剪贴板,则您尝试打开它将失败。

恰好终端服务跟踪剪贴板,在旧版Windows(Vista之前)上,您必须打开剪贴板才能查看其中的内容...这最终会阻止您。唯一的解决方案是等待终端服务关闭剪贴板,然后再尝试。

重要的是要意识到,这不仅限于终端服务:任何情况都可能发生。在Win32中使用剪贴板是一个巨大的竞争条件。但是,由于设计上您只应该响应用户输入而混淆剪贴板,因此通常不会出现问题。


31

我知道这个问题很旧,但问题仍然存在。如前所述,在系统剪贴板被其他进程阻塞时会出现此异常。不幸的是,有许多截屏工具、截图程序和文件复制工具可以阻止Windows剪贴板。因此,每当你在电脑上安装了这样的工具并尝试使用Clipboard.SetText(str)时,都会出现此异常。

解决方法:

永远不要使用

Clipboard.SetText(str);

使用替代方案

Clipboard.SetDataObject(str);

1
@K_Rol:看起来Yishai Galatzer的回答已经解释了这个问题。 - Cameron
我得到了“无法将类型为'String'的值转换为'DataObject'”的错误。 - AndruWitta
顺便提一下,我不得不使用 Clipboard.SetDataObject(str, true); 来使剪贴板数据在应用程序外可访问。 - deadlydog

13

我使用本地的Win32函数解决了自己应用程序的这个问题:OpenClipboard(),CloseClipboard()和SetClipboardData()。

下面是我制作的包装类。能否请有人检查一下并告诉我它是否正确。特别是当托管代码作为x64应用程序运行时(我在项目选项中使用任何CPU)。如果从x64应用程序链接到x86库会发生什么?

谢谢!

这是代码:

public static class ClipboardNative
{
    [DllImport("user32.dll")]
    private static extern bool OpenClipboard(IntPtr hWndNewOwner);

    [DllImport("user32.dll")]
    private static extern bool CloseClipboard();

    [DllImport("user32.dll")]
    private static extern bool SetClipboardData(uint uFormat, IntPtr data);

    private const uint CF_UNICODETEXT = 13;

    public static bool CopyTextToClipboard(string text)
    {
        if (!OpenClipboard(IntPtr.Zero)){
            return false;
        }

        var global = Marshal.StringToHGlobalUni(text);

        SetClipboardData(CF_UNICODETEXT, global);
        CloseClipboard();

        //-------------------------------------------
        // Not sure, but it looks like we do not need 
        // to free HGLOBAL because Clipboard is now 
        // responsible for the copied data. (?)
        //
        // Otherwise the second call will crash
        // the app with a Win32 exception 
        // inside OpenClipboard() function
        //-------------------------------------------
        // Marshal.FreeHGlobal(global);

        return true;
    }
}

1
PS:我也尝试在调用“本地”函数之前使用了托管的 Clipboard.SetText() 调用(即仅在托管版本无效时使用本地方式)。但是如果托管版本失败,它会锁定剪贴板,此后本地版本也无法打开剪贴板。 - Mar
2
这个答案太棒了<3,谢谢。我使用的是 .NET Framework 4.8,其他方法似乎都没有帮助。 - hakamairi
1
这应该是最佳答案,它是最安全的方法。 - Christoph B
请注意,在 .net 6(以及可能的较低版本)中,这个方法在使用 WPF 时无法正常工作。您需要指定一个有效的窗口句柄(HWND),而不是使用 IntPtr.Zero 来调用 OpenClipboard 方法,这样才能正常运行。 - MHolzmayr
@MHolzmayr,谢谢,我会检查并更新上面的代码!我的回答是针对.NET Framework 4的,我还没有切换到.NET。 - Mar
这个解决方案存在一个问题,虽然它可以将文本复制到剪贴板,但无法粘贴到WPF TextBox或记事本中,因为它们只支持纯文本区域。对于VSCode来说,可以正常粘贴。 - CodingNinja

10

实际上可能还存在另一个问题。框架调用(包括 WPF 和 Winform 版本)类似于这样的东西(代码来自于反编译):

private static void SetDataInternal(string format, object data)
{
    bool flag;
    if (IsDataFormatAutoConvert(format))
    {
        flag = true;
    }
    else
    {
        flag = false;
    }
    IDataObject obj2 = new DataObject();
    obj2.SetData(format, data, flag);
    SetDataObject(obj2, true);
}
请注意,在这种情况下,SetDataObject总是以true为参数调用。
在内部,这会触发两个win32 api调用,一个用于设置数据,另一个用于从应用程序中刷新数据,以便在应用程序关闭后仍然可用。
我见过几个应用程序(一些Chrome插件和下载管理器),它们监听剪贴板事件。一旦第一个调用到达,应用程序将打开剪贴板以查看数据,并且第二个调用刷新将失败。
除了编写自己的使用直接 win32 API 的剪贴板类或直接使用 false 调用 setDataObject 以保留应用程序关闭后的数据之外,还没有找到好的解决方案。

2

那不是解决方案,只是一些额外的信息,可以在所有解决方案都在您的PC上工作但在其他地方失败时如何重现它。如采纳的答案所述 - 剪贴板可能被其他应用程序占用。您只需要正确处理此故障,以说明用户为什么无法正常工作。

因此,只需使用下面的几行创建一个新的控制台应用程序并运行它。而在它运行时-测试您的主要应用程序,看它如何处理繁忙的剪贴板:

using System;
using System.Runtime.InteropServices;

namespace Clipboard
{
    class Program
    {
        [DllImport("user32.dll")]
        private static extern bool OpenClipboard(IntPtr hWndNewOwner);

        [DllImport("user32.dll")]
        private static extern bool CloseClipboard();

        static void Main(string[] args)
        {
            bool res = OpenClipboard(IntPtr.Zero);
            Console.Write(res);
            Console.Read();
            CloseClipboard();
        }
    }
}

2

使用WinForms版本(在WPF应用程序中使用WinForms没有任何问题),它可以处理您所需的一切:

System.Windows.Forms.Clipboard.SetDataObject(yourText, true, 10, 100);

这将尝试将"yourText"复制到剪贴板,即使应用程序退出后,它仍然存在;最多重试10次,并在每次尝试之间等待100毫秒。
参考链接:https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.clipboard.setdataobject?view=netframework-4.7.2#System_Windows_Forms_Clipboard_SetDataObject_System_Object_System_Boolean_System_Int32_System_Int32_

1
这在我的WPF应用程序中发生了。我遇到了OpenClipboard Failed (Exception from HRESULT: 0x800401D0 (CLIPBRD_E_CANT_OPEN))的问题。
我使用了:
ApplicationCommands.Copy.Execute(null, myDataGrid);

解决方法是先清空剪贴板。
Clipboard.Clear();
ApplicationCommands.Copy.Execute(null, myDataGrid);

如果“Beyond Clipboard”正在运行,则Clipboard.Clear()将引发完全相同的异常。已使用“Beyond Compare”版本4.2.3.22587进行测试。 - itsho

0
在WPF中,Cliboard.SetText和Cliboard.SetDataObject之间的区别在于文本不会被复制到剪贴板,只有指针。我检查了源代码。如果我们调用SetDataObject(data, true),Cliboard.Flush()也将被调用。由此,即使在关闭应用程序后,文本或数据仍然可用。我认为Windows应用程序只在关闭时调用Flush()。由此,它节省了内存,同时提供了对没有活动应用程序的数据的访问权限。
复制到剪贴板:
IDataObject CopyStringToClipboard(string s)
{
  var dataObject = new DataObject(s);
  Clipboard.SetDataObject(dataObject, false);
  return dataObject;
}

应用程序或窗口关闭时的代码:

try
{
  if ((clipboardData != null) && Clipboard.IsCurrent(clipboardData))
    Clipboard.Flush();
}
catch (COMException ex) {}

clipboardData是一个窗口类的字段或静态变量。


0
如果你在设置-系统-剪贴板中开启了剪贴板历史记录,会导致CLIPBRD_E_CANT_OPEN错误。此外,当你的应用程序退出时,SetDataObject(object, true);会保留你复制到剪贴板的内容。如果将其设置为false,则系统会清空剪贴板。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接