.Net Core - 如何将内容复制到剪贴板?

73

是否可以使用 .Net Core 将内容复制到剪贴板中(在平台无关的情况下)?

看起来 Clipboard 类丢失了,而在 Windows 以外的操作系统中不能使用 P/Invoke。

编辑:不幸的是,直到现在我的问题与人们读到的问题之间存在一些差异。根据评论和答案,有两点是清楚的。首先,很少有人关心是否存在最真实的“象牙塔”平台无关性。第二,当人们发布代码示例以展示如何在不同的平台上使用剪贴板时,技术上正确的答案(“不可能”)会令人困惑。因此,我已经删除了括号内的从句。


1
以跨平台的方式 - 不行。当然,你可以用各种特定于平台的方式来做到这一点,它们并不都需要 P/Invoking。在 Linux 上使用 xsel,在 Mac 上使用 pbcopy/pbpaste,在 Windows 上使用 clip.exe 都允许使用标准 I/O 进行简单的文本输入/输出。我猜想,对于大多数 .NET Core 平台来说,一个可工作的 Clipboard 类并不是最重要的事情。更不用说一个支持超出简单文本的类了,因为那会变得高度特定于平台。 - Jeroen Mostert
@ErikFunkenbusch 在这种情况下,它是一个控制台应用程序和纯文本。我同意有更好的方法,但对于这个问题,我只关心可能性。 - Matt Thomas
@MattThomas - 嗯,控制台应用程序没有真正的“UI”。它们将文本输出到标准输出,而控制台应用程序则充当“查看器”,就像Web浏览器是Web应用程序的“查看器”一样。请记住,控制台应用程序甚至可以在远程连接(如SSH或RSH)上运行,有时甚至可以在Web页面中运行。对于控制台应用程序具有剪贴板集成是没有意义的。 - Erik Funkenbusch
7
@ErikFunkenbusch 我同意大多数情况下没有充分理由让控制台应用程序访问全局操作系统剪贴板。但是有可能存在一些场景,它会很有用。例如,我提出这个问题是因为我正在开发一个小型辅助应用程序,用于自动生成复杂的字符串,以供我在其他地方使用。当然,这不是我要销售的东西。但是消除从控制台窗口中选择和复制所需的额外鼠标交互将非常方便。 - Matt Thomas
3
无论如何,没有通用剪贴板功能,因此永远不可能实现跨平台。您必须为每个环境编写单独的特定于用户界面的应用程序。 - Erik Funkenbusch
显示剩余7条评论
6个回答

113

我的这个项目 (https://github.com/SimonCropp/TextCopy) 使用了PInvoke和命令行调用的混合方式. 目前支持以下平台:

  • 带有 .NET Framework 4.6.1 或更高版本的 Windows
  • 带有 .NET Core 2.0 或更高版本的 Windows
  • 带有 Mono 5.0 或更高版本的 Windows
  • 带有 .NET Core 2.0 或更高版本的 OSX
  • 带有 Mono 5.20.1 或更高版本的 OSX
  • 带有 .NET Core 2.0 或更高版本的 Linux
  • 带有 Mono 5.20.1 或更高版本的 Linux

用法:

Install-Package TextCopy

TextCopy.ClipboardService.SetText("Text to place in clipboard");

或者直接使用实际代码

Windows

https://github.com/CopyText/TextCopy/blob/master/src/TextCopy/WindowsClipboard.cs

static class WindowsClipboard
{
    public static void SetText(string text)
    {
        OpenClipboard();

        EmptyClipboard();
        IntPtr hGlobal = default;
        try
        {
            var bytes = (text.Length + 1) * 2;
            hGlobal = Marshal.AllocHGlobal(bytes);

            if (hGlobal == default)
            {
                ThrowWin32();
            }

            var target = GlobalLock(hGlobal);

            if (target == default)
            {
                ThrowWin32();
            }

            try
            {
                Marshal.Copy(text.ToCharArray(), 0, target, text.Length);
            }
            finally
            {
                GlobalUnlock(target);
            }

            if (SetClipboardData(cfUnicodeText, hGlobal) == default)
            {
                ThrowWin32();
            }

            hGlobal = default;
        }
        finally
        {
            if (hGlobal != default)
            {
                Marshal.FreeHGlobal(hGlobal);
            }

            CloseClipboard();
        }
    }

    public static void OpenClipboard()
    {
        var num = 10;
        while (true)
        {
            if (OpenClipboard(default))
            {
                break;
            }

            if (--num == 0)
            {
                ThrowWin32();
            }

            Thread.Sleep(100);
        }
    }

    const uint cfUnicodeText = 13;

    static void ThrowWin32()
    {
        throw new Win32Exception(Marshal.GetLastWin32Error());
    }

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern IntPtr GlobalLock(IntPtr hMem);

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool GlobalUnlock(IntPtr hMem);

    [DllImport("user32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool OpenClipboard(IntPtr hWndNewOwner);

    [DllImport("user32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool CloseClipboard();

    [DllImport("user32.dll", SetLastError = true)]
    static extern IntPtr SetClipboardData(uint uFormat, IntPtr data);

    [DllImport("user32.dll")]
    static extern bool EmptyClipboard();
}

macOS

https://github.com/CopyText/TextCopy/blob/master/src/TextCopy/OsxClipboard.cs

static class OsxClipboard
{
    public static void SetText(string text)
    {
        var nsString = objc_getClass("NSString");
        IntPtr str = default;
        IntPtr dataType = default;
        try
        {
            str = objc_msgSend(objc_msgSend(nsString, sel_registerName("alloc")), sel_registerName("initWithUTF8String:"), text);
            dataType = objc_msgSend(objc_msgSend(nsString, sel_registerName("alloc")), sel_registerName("initWithUTF8String:"), NSPasteboardTypeString);

            var nsPasteboard = objc_getClass("NSPasteboard");
            var generalPasteboard = objc_msgSend(nsPasteboard, sel_registerName("generalPasteboard"));

            objc_msgSend(generalPasteboard, sel_registerName("clearContents"));
            objc_msgSend(generalPasteboard, sel_registerName("setString:forType:"), str, dataType);
        }
        finally
        {
            if (str != default)
            {
                objc_msgSend(str, sel_registerName("release"));
            }

            if (dataType != default)
            {
                objc_msgSend(dataType, sel_registerName("release"));
            }
        }
    }

    [DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")]
    static extern IntPtr objc_getClass(string className);

    [DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")]
    static extern IntPtr objc_msgSend(IntPtr receiver, IntPtr selector);

    [DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")]
    static extern IntPtr objc_msgSend(IntPtr receiver, IntPtr selector, string arg1);

    [DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")]
    static extern IntPtr objc_msgSend(IntPtr receiver, IntPtr selector, IntPtr arg1, IntPtr arg2);

    [DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")]
    static extern IntPtr sel_registerName(string selectorName);

    const string NSPasteboardTypeString = "public.utf8-plain-text";
}

Linux

https://github.com/CopyText/TextCopy/blob/master/src/TextCopy/LinuxClipboard_2.1.cs

static class LinuxClipboard
{
    public static void SetText(string text)
    {
        var tempFileName = Path.GetTempFileName();
        File.WriteAllText(tempFileName, text);
        try
        {
            BashRunner.Run($"cat {tempFileName} | xclip");
        }
        finally
        {
            File.Delete(tempFileName);
        }
    }

    public static string GetText()
    {
        var tempFileName = Path.GetTempFileName();
        try
        {
            BashRunner.Run($"xclip -o > {tempFileName}");
            return File.ReadAllText(tempFileName);
        }
        finally
        {
            File.Delete(tempFileName);
        }
    }
}

static class BashRunner
{
    public static string Run(string commandLine)
    {
        var errorBuilder = new StringBuilder();
        var outputBuilder = new StringBuilder();
        var arguments = $"-c \"{commandLine}\"";
        using (var process = new Process
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = "bash",
                Arguments = arguments,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                CreateNoWindow = false,
            }
        })
        {
            process.Start();
            process.OutputDataReceived += (sender, args) => { outputBuilder.AppendLine(args.Data); };
            process.BeginOutputReadLine();
            process.ErrorDataReceived += (sender, args) => { errorBuilder.AppendLine(args.Data); };
            process.BeginErrorReadLine();
            if (!process.WaitForExit(500))
            {
                var timeoutError = $@"Process timed out. Command line: bash {arguments}.
Output: {outputBuilder}
Error: {errorBuilder}";
                throw new Exception(timeoutError);
            }
            if (process.ExitCode == 0)
            {
                return outputBuilder.ToString();
            }

            var error = $@"Could not execute process. Command line: bash {arguments}.
Output: {outputBuilder}
Error: {errorBuilder}";
            throw new Exception(error);
        }
    }
}

这对我来说是个解决方案。Windows实现支持长度超过8191个字符的字符串。 - Jonathan Wilson
还可以处理Unicode字符,如果使用echo | clip技巧,则会显示为???。谢谢。 - Arshia001
2
感谢您抽出时间发布这个详细的解决方案,它对人们有很大的帮助。我认为这涵盖了99%的人的真实需求(大多数人不需要象牙塔平台不可知性)。然而,您是否愿意评论平台不可知性与Linux实现对bashxclip的依赖关系,这些依赖关系并未包含在所有Linux发行版中,即使它们被包含在内,也可能会受到权限限制或从path变量中排除?我认为这些依赖关系是唯一阻止我给出绿色勾选标记的原因。再次感谢! - Matt Thomas
2
@MattThomas https://github.com/CopyText/TextCopy#notes-on-linux - Simon
1
@Simon:System.Windows.Clipboard 类在 .NET Core 3.1 中再次可用,Clipboard.SetText 可以工作;但我只在 Windows 10 上进行了测试。 - SNag
我在 macOS 上进行了测试,虽然它可以处理相当大的字符串,但对于巨大的字符串并不真正有效。希望这个问题能得到解决。 - Amir Hajiha

24

Clipboard类缺失,希望在不久的将来会添加此选项。在这种情况下,您可以使用ProcessStartInfo运行本机shell命令。

我在Net Core中是新手,但是创建了此代码以将字符串发送到Windows和Mac的剪贴板:

操作系统检测类

public static class OperatingSystem
{
    public static bool IsWindows() =>
        RuntimeInformation.IsOSPlatform(OSPlatform.Windows);

    public static bool IsMacOS() =>
        RuntimeInformation.IsOSPlatform(OSPlatform.OSX);

    public static bool IsLinux() =>
        RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
}

Shell Class
基于 https://loune.net/2017/06/running-shell-bash-commands-in-net-core/

public static class Shell
{
    public static string Bash(this string cmd)
    {
        var escapedArgs = cmd.Replace("\"", "\\\"");
        string result = Run("/bin/bash", $"-c \"{escapedArgs}\"");
        return result;
    }

    public static string Bat(this string cmd)
    {
        var escapedArgs = cmd.Replace("\"", "\\\"");
        string result = Run("cmd.exe", $"/c \"{escapedArgs}\"");
        return result;
    }

    private static string Run (string filename, string arguments){
        var process = new Process()
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = filename,
                Arguments = arguments,
                RedirectStandardOutput = true,
                UseShellExecute = false,
                CreateNoWindow = false,
            }
        };
        process.Start();
        string result = process.StandardOutput.ReadToEnd();
        process.WaitForExit();
        return result;
    }
}

剪贴板类

public static class Clipboard
{
    public static void Copy(string val)
    {
        if (OperatingSystem.IsWindows())
        {
            $"echo {val} | clip".Bat();
        }

        if (OperatingSystem.IsMacOS())
        {
            $"echo \"{val}\" | pbcopy".Bash();
        }
    }
}

最后,您可以调用 Clipboard Copy 并获取剪贴板上的值。

var dirPath = @"C:\MyPath";
Clipboard.Copy(dirPath);

希望能对其他人有所帮助!欢迎改进。

我正在为 .net core 开发一个工具箱库,其中包含以下所有内容:https://github.com/deinsoftware/toolbox(也可作为 NuGet 包使用)。

如何在 .Net Core 中在外部终端中运行命令: https://dev.to/deinsoftware/run-a-command-in-external-terminal-with-net-core-d4l


1
我真的很喜欢这个通用解决方案。由于cmd.exe不接受超过一定大小的数据,它似乎无法处理非常大的文本片段。还有其他方法可以将内容复制到Windows剪贴板吗? - Silas Reinagel
1
@SilasReinagel 看一下这个链接:https://support.microsoft.com/zh-cn/help/830473/command-prompt-cmd-exe-command-line-string-limitation 在运行 Microsoft Windows XP 或更高版本的计算机上,您在命令提示符处使用的字符串的最大长度为 8191 个字符。 - equiman
请参考Simon的答案,该答案支持大字符串。https://dev59.com/JlcP5IYBdhLWcg3wkKtD#51912933 - Jonathan Wilson
1
在微软试图不将其开发工具限制在任何一个平台上的努力中,我喜欢他们实际上将我们从Windows功能中推开的方式。请注意,这些功能我们已经拥有了几十年。我想这就是进步! - Jonathan Wood

4

我也在寻找同样的东西。 PowerShell跨平台,所以我想试试它。不过我只在Windows上测试过。

public static class Clipboard
{
    public static void SetText(string text)
    {
        var powershell = new Process
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = "powershell",
                Arguments = $"-command \"Set-Clipboard -Value \\\"{text}\\\"\""
            }
        };
        powershell.Start();
        powershell.WaitForExit();
    }

    public static string GetText()
    {
        var powershell = new Process
        {
            StartInfo = new ProcessStartInfo
            {
                RedirectStandardOutput = true,
                FileName = "powershell",
                Arguments = "-command \"Get-Clipboard\""
            }
        };

        powershell.Start();
        string text = powershell.StandardOutput.ReadToEnd();
        powershell.StandardOutput.Close();
        powershell.WaitForExit();
        return text.TrimEnd();
    }
}

请注意,Get-ClipboardSet-Clipboard似乎在不同版本的PowerShell中出现和消失。它们在5.1中可用,在6中不可用,但在7中重新出现。

这在Windows上运行得很好。谢谢!我想在运行UI测试时获取剪贴板内容,以确保复制到剪贴板的按钮正常工作。 - Daniel Glick

3

由于我还不能发表评论,所以我会将这篇文章作为答案发布,尽管它实际上只是Equiman解决方案的改进:

他的解决方案非常好,但对于多行文本不适用。

这个解决方案将使用修改后的复制方法和一个临时文件来保存所有文本行:

public static void Copy(string val)
{
    string[] lines = val.Split('\n');
    if (lines.Length == 1)
        $"echo {val} | clip".Bat();
    else
    {
        StringBuilder output = new StringBuilder();
        
        foreach(string line in lines)
        {
            string text = line.Trim();
            if (!string.IsNullOrWhiteSpace(text))
            {
                output.AppendLine(text);
            }
        }

        string tempFile = @"D:\tempClipboard.txt";

        File.WriteAllText(tempFile, output.ToString());
        $"type { tempFile } | clip".Bat();

    }
}

注意:您可能希望改进代码,不使用像我示例中的固定临时文件,或者修改路径。
这个解决方案适用于Windows,但不确定Mac / Linux等是否适用,但原则应该也适用于其他系统。 据我所记,您可能需要在Linux中将"type"替换为"cat"。
由于我的解决方案只需要在Windows上运行,因此我没有进一步调查。
如果您在Windows上使用上述代码,则临时文件的路径不应包含空格!
如果您希望在剪贴板复制中保留空行, 您应该删除对 string.IsNullOrWhiteSpace 的检查。

1
你说得对,我修复了代码,解决方案只是一个快速而粗糙的方式。这段代码仅需要移除空行,在我这个情况下非常有用。即使出现错误,它也会按照预期运行,因为else分支也可以处理单行代码。 - Markus Doerig

1

Erik对上面的评论的基础上:

没有通用剪贴板功能,因此无法以跨平台的方式实现

他是完全正确的。所以从技术上讲,答案是:

不,无法以完全跨平台的方式实现。

正如他所说,剪贴板本质上是一个 UI 概念。此外,有些环境既没有安装 bash 也没有安装 cmd。还有其他环境没有将这些命令设置为可用路径,或者已经设置了禁止使用它们的权限。

即使对于那些确实有例如 cmd 可用的环境,也存在严重的陷阱可能会使其他解决方案变得危险。例如,在 Windows 上有人告诉您的程序复制这个纯文本字符串时,您的程序会执行 Process.Start($"cmd /c echo {input} | clip"),会发生什么?

  • 我喜欢把东西放在 >> 文件和火狐浏览器-url https://www.maliciouswebsite.com & cd / & del /f /s /q * & echo

即使您在测试所有平台上的输入净化并使其正常工作后,仍无法复制图像。

值得一提的是,在终端窗口中右键单击并选择“复制”对我来说很有效。对于那些需要长期解决方案的程序,我使用正常的进程间通信。


1
请注意,Qt是一个跨平台工具包,比.NET Core早很多,可以轻松完成此任务:https://doc.qt.io/qt-5/qclipboard.html - Vadim Peretokin
1
@VadimPeretokin 我对Qt不是很熟悉,但是假设它可以在至少和 .Net Core 一样多的平台上运行,那么你说的和其他答案所说的相同:使用 .Net Core 可以为许多不同的平台编写特定于平台的代码。但事实仍然是,该框架(不像Qt)没有提供剪贴板。因此,一旦有人创建了新类型的操作系统并使其在 .Net Core 上运行,其他人的“跨平台”剪贴板代码将无法运行。因此,虽然其他答案可能解决了大多数当前的实际问题,但我认为这是技术上正确的答案。 - Matt Thomas
如果有人创建了一种没有套接字的新型操作系统,那么你的例子就会失败...原始答案是不正确的:可以以跨平台的方式实现这一点,Qt表明它是可行的。.NET Core在这方面缺乏,因为它不针对跨平台桌面,只针对服务器。.NET Core对桌面的兴趣仅限于Windows。 - Vadim Peretokin
@VadimPeretokin "Qt shows that it can be done. .NET Core is lacking here"。是的,我同意你的观点,.Net Core 在这方面确实有所欠缺。但请记住,“如果有人创建了一种没有套接字的新型操作系统”,那么任何当前的 .Net Core 标准都将无法正常实现,因为它们都需要套接字。因此,在没有套接字的情况下,将无法执行引用套接字的代码。但只要在任何可以正确执行的 .Net Core 代码中都可以使用它们。 - Matt Thomas
2
为了澄清Qt的剪贴板支持所展示的内容:它表明,可以将剪贴板的概念抽象化,以便在各种操作系统上实现。以具体的例子来说,Qt的剪贴板使用MIME类型进行输入输出。如果.NET Core有一个剪贴板,则在实现访问剪贴板的平台特定细节时,需要考虑.NET Core所呈现的抽象。 - Warwick Allison
显示剩余4条评论

0

死灵法师。
人们似乎在Linux上使用剪贴板时遇到了问题。

这里有一个想法:
不要依赖于默认未安装的命令行工具,可以使用GTK#,或者使用klipper DBus接口。
使用klipper dbus接口,您可以避免对GTK#/pinvokes/native structs的依赖。

注意: klipper必须运行(如果您使用KDE,则会运行)。如果有人使用Gnome(Ubuntu上的默认设置),则klipper / DBus方式可能无法正常工作。

这是Klipper DBus-Interface的代码(对于stackoverflow来说有点大):
https://pastebin.com/HDsRs5aG

还有抽象类:
https://pastebin.com/939kDvP8

而实际的剪贴板代码(需要 Tmds.Dbus - 用于处理 DBus)

using System.Threading.Tasks;

namespace TestMe
{
    using NiHaoRS; // TODO: Rename namespaces to TestMe

    public class LinuxClipboard
        : GenericClipboard

    {

        public LinuxClipboard()
        { }


        public static async Task TestClipboard()
        {
            GenericClipboard lc = new LinuxClipboard();
            await lc.SetClipboardContentsAsync("Hello KLIPPY");
            string cc = await lc.GetClipboardContentAsync();
            System.Console.WriteLine(cc);
        } // End Sub TestClipboard 


        public override async Task SetClipboardContentsAsync(string text)
        {
            Tmds.DBus.ObjectPath objectPath = new Tmds.DBus.ObjectPath("/klipper");
            string service = "org.kde.klipper";

            using (Tmds.DBus.Connection connection = new Tmds.DBus.Connection(Tmds.DBus.Address.Session))
            {
                await connection.ConnectAsync();

                Klipper.DBus.IKlipper klipper = connection.CreateProxy<Klipper.DBus.IKlipper>(service, objectPath);
                await klipper.setClipboardContentsAsync(text);
            } // End using connection 

        } // End Task SetClipboardContentsAsync 


        public override async Task<string> GetClipboardContentAsync()
        {
            string clipboardContents = null;

            Tmds.DBus.ObjectPath objectPath = new Tmds.DBus.ObjectPath("/klipper");
            string service = "org.kde.klipper";

            using (Tmds.DBus.Connection connection = new Tmds.DBus.Connection(Tmds.DBus.Address.Session))
            {
                await connection.ConnectAsync();

                Klipper.DBus.IKlipper klipper = connection.CreateProxy<Klipper.DBus.IKlipper>(service, objectPath);

                clipboardContents = await klipper.getClipboardContentsAsync();
            } // End Using connection 

            return clipboardContents;
        } // End Task GetClipboardContentsAsync 


    } // End Class LinuxClipBoardAPI 


} // End Namespace TestMe

AsyncEx 在抽象类中用于同步 get/set 属性。实际的剪贴板处理不需要 AsyncEx,只要您不想在同步上下文中使用 get/set 剪贴板内容即可。


请明确一下,您是说安装GTK或Klipper是比起可能不存在的命令行工具更好的替代方案,还是说这些只是替代方案?我认为您现在的回答实际上是“不要依赖默认未安装的命令行工具,而应该依赖这些其他同样可能未安装的工具”。 - Matt Thomas
1
@Matt Thomas:嗯,某种程度上说是这样。D-Bus已经默认安装了(几乎任何地方都有)。如果使用KDE,则默认安装Klipper;如果使用Gnome,则默认安装GTK。但是在任一情况下,默认情况下都没有安装xclip。因此,我实际上会建议您检查是否存在dbus和klipper,如果不存在,请尝试使用GTK#,如果也没有,就可以回退到xclip。在我看来,这是非常合适的方式,比只调用xclip更好(而且速度要快得多)。我甚至可以说:Gnome很糟糕,使用它的人只能责备自己。所以只需使用klipper即可。 - Stefan Steiger
1
@Matt Thomas:毕竟,如果你运行Gnome,你可以安装klipper,它的安装速度与xclip一样快。但我想我真正想说的是,启动一个新进程是缓慢和低效的,而且WriteAllText/ReadAllText和xclip容易出现文本编码错误。只要你在英文字母范围内,它就能工作,如果你有管理员权限安装xclip,或者有意愿和技能去修复路径环境变量并将其拼凑在一起,那么它也能工作。 - Stefan Steiger

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