如何解决“无法打开剪贴板:访问被拒绝”错误?

19

我正在使用以下代码将文本复制到剪贴板:

  Clipboard.Open;
  try
    Clipboard.AsText := GenerateClipboardText;
  finally
    Clipboard.Close;
  end;

我偶尔会收到“无法打开剪贴板:访问被拒绝”错误消息。我猜测这些错误是由于其他应用程序锁定了剪贴板,但我似乎从未做过任何应该导致这些锁定的其他应用程序相关操作。

奇怪的是,我的用户似乎更多地报告了在Vista和Windows 7上出现此类错误而非XP。

在尝试访问剪贴板之前,有没有一种方法可以检查它是否被锁定?


1
请注意来自 Delphi 文档的以下代码片段:"Clipboard.Open -> 打开剪贴板,防止其他应用程序更改其内容,直到关闭剪贴板。在向剪贴板添加一系列项目之前调用 Open。这可以防止其他应用程序在关闭剪贴板之前覆盖它。(当向剪贴板添加单个项目时,无需调用 Open。)" - Gabriel
6个回答

25

这不是一个Delphi的问题。因为剪贴板随时可能被锁定,即使你检查了剪贴板当前是否未被锁定,检查后它也可能立即被锁定。

你有两种可能性:

  1. 不使用Delphi剪贴板类,而是使用原始API函数,这样你就可以更精细地控制可能的错误情况。
  2. 期望你的代码失败并添加异常处理程序。然后添加一些重试代码,例如尝试三次设置文本,可能具有指数退避,在抛出自己的错误之前。

我推荐第二个解决方案,因为它将是更像Delphi的方法,并最终导致更清洁的代码。

var
  Success : boolean;
  RetryCount : integer;
begin
  RetryCount := 0;
  Success := false;
  while not Success do
    try
      //
      // Set the clipboard here
      //
      Success := True;
    except
      on E: EClipboardException do
      begin
        Inc(RetryCount);
        if RetryCount < 3 then
          Sleep(RetryCount * 100)
        else
          raise Exception.Create('Cannot set clipboard after three attempts');
      end else
        raise;  // if not a clipboard problem then re-raise 
    end;
end;

4
这并不是Win32的问题,而是编写并发系统时的一个简单事实。 - mghie
1
为什么他们不等待全局锁呢?这明显是 Delphi 的问题。 - nurettin

9
“奇怪的是,我的用户似乎报告的错误比XP更多地涉及到Vista和Windows 7。”
这可能与Vista/Win7如何处理剪贴板查看器通知有关。虽然它们仍支持XP“剪贴板查看器链”,该链发送一条必须依次重新发送给每个侦听器的通知消息(如果一个应用程序未能执行此操作,则其他应用程序将不会收到通知)。从Vista开始,应用程序直接收到通知。没有任何东西阻止它们同时尝试访问剪贴板。
类比:我有三个孩子。我有一个蛋糕。按照XP规则,我告诉最大的孩子吃点蛋糕,然后告诉下一个最大的孩子切一块。她得到了她的那一块,告诉她的哥哥,他得到了他的那一块,然后告诉他的弟弟,他得到了他的那一块,一切都按照有序的方式进行。
问题:中间的孩子把蛋糕带到他的房间里,没有告诉最小的孩子,所以最小的孩子错过了。
在Vista/Windows7中,这个系统仍然存在。但是新的应用程序可以要求我在蛋糕到达厨房时立即通知它们。我大喊“蛋糕准备好了!”,他们同时出现并尝试抢夺一些蛋糕。但是只有一把刀,所以他们不断地伸手去拿刀,却没有成功,等待下一个机会。

2

尝试检查GetClipboardOwner,如果它不是null并且不是你的Application.Handle,则无法打开以修改其内容。
即使看起来可以,当你实际操作时可能会发生变化。
因此,在循环中添加try except,直到成功获取或优雅地放弃(例如通知用户)。


1
首先请注意,这可能不是您的应用程序的问题。其他应用程序锁定了剪贴板或混乱了通知链,现在您的应用程序无法访问它。当我遇到这样的问题时,我会重新启动计算机,它们就神奇地消失了...好吧...至少直到我再次运行创建问题的应用程序。

此代码(未在Delphi中检查)可能会对您有所帮助。如果通知链断开了(除了PC重新启动外,没有任何东西可以修复它),则它将无法解决问题,但如果某个应用程序锁定了剪贴板一段时间,则可以解决问题。如果那个讨厌的应用程序将剪贴板锁定了很长时间(几秒钟),请增加MaxRetries:

procedure Str2Clipboard(CONST Str: string; iDelayMs: integer);
CONST
   MaxRetries= 5;
VAR RetryCount: Integer;
begin
 RetryCount:= 0;
 for RetryCount:= 1 to MaxRetries DO
  TRY
    inc(RetryCount);
    Clipboard.AsText:= Str;
    Break;
  EXCEPT
    on Exception DO
      if RetryCount = MaxRetries
      then RAISE Exception.Create('Cannot set clipboard')
      else Sleep(iDelayMs)
  END;
end;

此外,将“raise”删除并将其转换为函数并像这样使用可能是一个好主意:
if not Str2Clipboard 
then Log.AddMsg('Dear user, other applications are blocking the clipboard. We have tried. We really did. But it didn''t work. Try again in a few seconds.');

你的睡眠时间不够长。15毫秒什么也做不了。经过5次15毫秒后,可能仍然有打开剪贴板的程序。 - Chris Thornton
你可能是对的,但另一方面,如果你使用了长时间的延迟,由于剪贴板正在使用,你的应用程序可能会挂起几秒钟。也许每个程序员都应该决定多少“冻结”是可以接受的。 - Gabriel
更好的是:延迟甚至可以用作过程的参数。---我刚刚更新了代码。 - Gabriel
在我看来,你最好使用逐渐增加的延迟时间。这些事情本质上是不可预测的。你不知道哪个应用程序已经打开了剪贴板,或者为什么打开了。而且你也不知道有多少其他应用程序在剪贴板通知链中。在能够成功地独占访问剪贴板之前,你可能需要等待几秒钟。如果250毫秒不够,下次尝试500,然后是1000,再是2000。所以将(iDelayMs * RetryCount)相乘,就可以得到你想要的结果。 - Chris Thornton

1

没有办法检查某件事情,然后根据结果做其他事情并期望它不会失败,因为除非检查和操作是原子操作,否则另一个进程或线程可能并行执行相同的操作。

这适用于尝试打开剪贴板、打开文件、创建或删除目录 - 您应该简单地尝试执行它,可能在循环中尝试多次,并优雅地处理错误。


-3

我猜你正在Win 8或更高版本上运行你的应用程序。

只需右键点击你的App .exe文件,进入兼容性选项卡,将兼容模式更改为Windows XP或更低版本。它一定会起作用的!


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