224 lines
5.7 KiB
C#
224 lines
5.7 KiB
C#
using System;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using SysClipboard = System.Windows.Clipboard;
|
|
|
|
/// <summary>
|
|
/// 剪贴板帮助类(WinForms / WPF 通用)
|
|
/// 解决的问题:
|
|
/// 1. CLIPBRD_E_CANT_OPEN (0x800401D0) 等 COMException / ExternalException
|
|
/// (剪贴板被其他进程占用,如远程桌面、剪贴板管理器、杀毒软件、微信/QQ 等)
|
|
/// 2. 非 STA 线程调用导致的 ThreadStateException
|
|
/// 3. 空字符串 / null 导致的 ArgumentNullException
|
|
/// </summary>
|
|
public static class ClipboardHelper
|
|
{
|
|
/// <summary>默认重试次数</summary>
|
|
public static int DefaultRetryCount { get; set; } = 10;
|
|
|
|
/// <summary>默认重试间隔(毫秒)</summary>
|
|
public static int DefaultRetryDelayMs { get; set; } = 100;
|
|
|
|
#region 同步 API
|
|
|
|
/// <summary>复制文本,失败返回 false,不抛异常</summary>
|
|
public static bool TrySetText(string text, int retryCount = -1, int retryDelayMs = -1)
|
|
{
|
|
return TryExecute(() =>
|
|
{
|
|
if (string.IsNullOrEmpty(text))
|
|
SysClipboard.Clear();
|
|
else
|
|
SetDataObjectCore(text);
|
|
return true;
|
|
}, out _, retryCount, retryDelayMs);
|
|
}
|
|
|
|
/// <summary>复制任意数据(文本、文件列表、图片、DataObject 等)</summary>
|
|
public static bool TrySetData(object data, int retryCount = -1, int retryDelayMs = -1)
|
|
{
|
|
if (data == null) return false;
|
|
return TryExecute(() =>
|
|
{
|
|
SetDataObjectCore(data);
|
|
return true;
|
|
}, out _, retryCount, retryDelayMs);
|
|
}
|
|
|
|
/// <summary>读取文本,失败或无文本返回 null</summary>
|
|
public static string TryGetText(int retryCount = -1, int retryDelayMs = -1)
|
|
{
|
|
TryExecute(() => SysClipboard.ContainsText() ? SysClipboard.GetText() : null,
|
|
out string result, retryCount, retryDelayMs);
|
|
return result;
|
|
}
|
|
|
|
/// <summary>清空剪贴板</summary>
|
|
public static bool TryClear(int retryCount = -1, int retryDelayMs = -1)
|
|
{
|
|
return TryExecute(() => { SysClipboard.Clear(); return true; },
|
|
out _, retryCount, retryDelayMs);
|
|
}
|
|
|
|
/// <summary>复制文本,失败时抛出最后一次的异常</summary>
|
|
public static void SetText(string text, int retryCount = -1, int retryDelayMs = -1)
|
|
{
|
|
Execute(() =>
|
|
{
|
|
if (string.IsNullOrEmpty(text))
|
|
SysClipboard.Clear();
|
|
else
|
|
SetDataObjectCore(text);
|
|
return true;
|
|
}, retryCount, retryDelayMs);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 异步 API(不阻塞 UI 线程,建议在 UI 线程 await 调用)
|
|
|
|
public static async Task<bool> TrySetTextAsync(string text, int retryCount = -1, int retryDelayMs = -1)
|
|
{
|
|
retryCount = retryCount < 0 ? DefaultRetryCount : retryCount;
|
|
retryDelayMs = retryDelayMs < 0 ? DefaultRetryDelayMs : retryDelayMs;
|
|
|
|
for (int i = 0; i < retryCount; i++)
|
|
{
|
|
try
|
|
{
|
|
RunSta(() =>
|
|
{
|
|
if (string.IsNullOrEmpty(text))
|
|
SysClipboard.Clear();
|
|
else
|
|
SetDataObjectCore(text);
|
|
return true;
|
|
});
|
|
return true;
|
|
}
|
|
catch (Exception ex) when (IsRetryable(ex))
|
|
{
|
|
if (i < retryCount - 1)
|
|
await Task.Delay(retryDelayMs).ConfigureAwait(true); // 回到 UI 线程
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public static async Task<string> TryGetTextAsync(int retryCount = -1, int retryDelayMs = -1)
|
|
{
|
|
retryCount = retryCount < 0 ? DefaultRetryCount : retryCount;
|
|
retryDelayMs = retryDelayMs < 0 ? DefaultRetryDelayMs : retryDelayMs;
|
|
|
|
for (int i = 0; i < retryCount; i++)
|
|
{
|
|
try
|
|
{
|
|
return RunSta(() => SysClipboard.ContainsText() ? SysClipboard.GetText() : null);
|
|
}
|
|
catch (Exception ex) when (IsRetryable(ex))
|
|
{
|
|
if (i < retryCount - 1)
|
|
await Task.Delay(retryDelayMs).ConfigureAwait(true);
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 内部实现
|
|
|
|
private static void SetDataObjectCore(object data)
|
|
{
|
|
SysClipboard.SetDataObject(data, true);
|
|
}
|
|
|
|
private static T Execute<T>(Func<T> action, int retryCount, int retryDelayMs)
|
|
{
|
|
retryCount = retryCount < 0 ? DefaultRetryCount : retryCount;
|
|
retryDelayMs = retryDelayMs < 0 ? DefaultRetryDelayMs : retryDelayMs;
|
|
if (retryCount < 1) retryCount = 1;
|
|
|
|
for (int i = 0; ; i++)
|
|
{
|
|
try
|
|
{
|
|
return RunSta(action);
|
|
}
|
|
catch (Exception ex) when (IsRetryable(ex) && i < retryCount - 1)
|
|
{
|
|
Thread.Sleep(retryDelayMs);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool TryExecute<T>(Func<T> action, out T result, int retryCount, int retryDelayMs)
|
|
{
|
|
try
|
|
{
|
|
result = Execute(action, retryCount, retryDelayMs);
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
result = default(T);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>剪贴板必须在 STA 线程访问;当前不是 STA 就新建一个 STA 线程执行</summary>
|
|
private static T RunSta<T>(Func<T> action)
|
|
{
|
|
if (Thread.CurrentThread.GetApartmentState() == ApartmentState.STA)
|
|
return action();
|
|
|
|
T result = default(T);
|
|
Exception error = null;
|
|
var thread = new Thread(() =>
|
|
{
|
|
try { result = action(); }
|
|
catch (Exception ex) { error = ex; }
|
|
});
|
|
thread.SetApartmentState(ApartmentState.STA);
|
|
thread.Start();
|
|
thread.Join();
|
|
|
|
if (error != null)
|
|
{
|
|
// 保留原始异常类型,便于上层判断
|
|
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(error).Throw();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 判断是否为"剪贴板暂时被占用"类的可重试异常
|
|
/// COMException 继承自 ExternalException,所以一起覆盖
|
|
/// </summary>
|
|
private static bool IsRetryable(Exception ex)
|
|
{
|
|
if (ex is ExternalException)
|
|
{
|
|
// 0x800401D0 CLIPBRD_E_CANT_OPEN
|
|
// 0x800401D1 CLIPBRD_E_CANT_EMPTY
|
|
// 0x800401D2 CLIPBRD_E_CANT_SET
|
|
// 0x800401D3 CLIPBRD_E_BAD_DATA
|
|
// 0x800401D4 CLIPBRD_E_CANT_CLOSE
|
|
// WinForms 的 "Requested Clipboard operation did not succeed" 也是 ExternalException
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#endregion
|
|
} |