Files
29WxOrder/WxOrder.Framework/Commons/ClipboardHelper.cs
T
chfei a217a74868 更新JSON格式化
增加剪切板helper
2026-10-19 10:17:43 +08:00

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
}