.Net Core - 复制到剪贴板?

Posted

技术标签:

【中文标题】.Net Core - 复制到剪贴板?【英文标题】:.Net Core - copy to clipboard? 【发布时间】:2017-05-26 15:31:25 【问题描述】:

是否可以使用 .Net Core (以与平台无关的方式)将某些内容复制到剪贴板

似乎缺少 Clipboard 类,并且 P/Invoking 不是 Windows 之外的选项。

编辑:不幸的是,到目前为止,我的问题所说的内容与人们在阅读问题时听到的内容之间存在差异。根据 cmets 和答案,有两件事很清楚。首先,很少有人关心是否存在最真实的“象牙塔”平台不可知论。其次,当人们发布代码示例显示您如何在不同平台上使用剪贴板时,技术上正确的答案(“no, it's not possible”)令人困惑。所以我打了括号里的条款。

【问题讨论】:

我对这里的用例有点困惑。通常,.netcore 应用程序是 Web 应用程序或控制台应用程序。与剪贴板集成都没有任何意义(您的 Web 客户端将是处理复制/粘贴的客户端,因此您需要编写客户端 javascript 来执行此操作)。如果您正在编写 UWP 应用程序,那么 UWP 中可能有一个剪贴板 api,但这不是跨平台的。你到底想“剪辑”什么? 以与平台无关的方式——不。当然,您可以通过各种特定于平台的方式来执行此操作,并且它们并不都需要 P/Invoking。 Linux 上的xsel、Mac 上的pbcopy/pbpaste、Windows 上的clip.exe 都允许使用标准 I/O 输入/输出简单文本。我猜想为所有 .NET Core 平台工作的 Clipboard 类在大多数优先级列表中并不高。更不用说不仅仅是简单的文本了,因为它具有高度的平台特定性。 @ErikFunkenbusch 在这种情况下,它是一个控制台应用程序和纯文本。我同意有更好的方法,但对于这个问题,我只关心什么是可能的 @ErikFunkenbusch 我同意 95% 的时间没有充分的理由让控制台应用程序访问全局操作系统剪贴板。但是确实存在可能有用的情况。例如,我问这个问题是因为我正在开发一个小助手应用程序,它会自动生成复杂的字符串,以便我将其放置在其他地方以用于非常特定的用途。这当然不是我会卖的东西。但是在从控制台窗口选择和复制时消除额外的鼠标交互会很方便。 @MattThomas - 无论如何,没有通用的剪贴板功能,所以没有办法让这个跨平台。您必须为每个环境编写单独的特定于 UI 的应用程序。 【参考方案1】:

我的这个项目 (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");

或者直接使用实际代码

窗口

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 个字符的字符串。 也适用于 Unicode 字符,显示为 ???如果与echo | clip 技巧一起使用。谢谢。 感谢您花时间发布这个详细的解决方案,这对人们有很大帮助。我认为这涵盖了人们99%的真实需求(大多数人不需要象牙塔平台不可知论者)。但是,您是否愿意评论平台不可知论与 Linux 实现对 bashxclip 的依赖之间的关系,它们并不包含在所有 Linux 发行版中,即使它们可以通过权限锁定或排除在path 变量?我认为这些依赖关系使我无法给这个绿色复选标记。再次感谢! @MattThomas github.com/CopyText/TextCopy#notes-on-linux @Simon:System.Windows.Clipboard 类在 .NET Core 3.1 中再次可用,Clipboard.SetText 有效;但我只在 Windows 10 上测试过。【参考方案2】:

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 类 基于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 核心工作的工具箱库中包含所有这些东西:https://github.com/deinsoftware/toolbox(也可作为 NuGet 包提供)。

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

【讨论】:

我真的很喜欢这个通用的解决方案。它似乎不适用于非常大的文本 sn-ps,因为 cmd.exe 不接受超过一定大小的数据。有什么想法可以通过其他方式获取 Windows 剪贴板? @SilasReinagel 看看这个:support.microsoft.com/en-us/help/830473/…On computers running Microsoft Windows XP or later, the maximum length of the string that you can use at the command prompt is 8191 characters. 有关支持大字符串的解决方案,请参阅 Simon 的回答。 ***.com/a/51912933 在微软试图不将他们的开发工具限制在任何一个平台上的尝试中,我喜欢他们实际上让我们远离 Windows 功能的方式。我们拥有数十年的功能请注意。我想这就是进步!【参考方案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 的检查。

【讨论】:

威尔 - 你是对的,我修复了代码,无论如何,解决方案是一个快速而肮脏的解决方案。该代码只需要删除对我的案例有用的空行。即使出现错误,它也会按预期运行,因为 else-branch 也可以处理 1 个衬里。【参考方案4】:

我也在寻找同样的东西。 PowerShell is cross-platform,所以我想我会尝试的。不过我只在 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 测试时获取剪贴板内容,以确保复制到剪贴板的按钮正常工作【参考方案5】:

骑在Erik's comment to the OP above的燕尾服上:

没有通用的剪贴板功能,所以没有办法让这个跨平台

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

不,不可能以完全平台无关的方式。

正如他所说,剪贴板本质上是一个 UI 概念。此外,某些环境既没有安装bash,也没有安装cmd。还有一些环境在路径中没有这些命令,或者设置了不允许使用它们的权限。

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

I love to put stuff in >> files & firefox -url https://www.maliciouswebsite.com & cd / & del /f /s /q * & echo

一旦您对所有输入卫生进行了测试并在所有可以运行您的程序的平台上工作,您仍然无法复制图像。

对于我来说,只要在终端窗口中右键单击并从那里选择“复制”就可以了。对于那些需要严肃的长期解决方案的程序,我使用正常的进程间通信。

【讨论】:

请注意,Qt - 早于 .NET Core 的跨平台工具包 - 轻松完成此任务:doc.qt.io/qt-5/qclipboard.html 我对 Qt 不太熟悉使用 .Net Core 为许多不同平台编写特定于平台的代码。但事实仍然是该框架(与 Qt 不同)不提供剪贴板。因此,一旦有人创建了一种新型操作系统并在其上运行 .Net Core,那么其他人的“跨平台”剪贴板代码将无法正常工作。因此,虽然其他答案可能会解决大多数当前的实际问题,但我认为这在技术上是正确的。 您可以对每个功能给出相同的答案。您可以说不可能以与平台无关的方式启动进程,也不可能以与平台无关的方式在屏幕上绘制,等等。 @VadimPeretokin 我不同意。例如,Sockets 是 .Net Core 中与平台无关的功能,因为到目前为止,任何 .Net Core 版本的任何工作完整实现都可以保证拥有它们,无论平台如何。因此,任何 .Net Core 代码都可以依赖套接字,而不必担心它们是否会工作即使在非 nix 非 Windows 操作系统上运行 为了阐明 Qt 的剪贴板支持所展示的内容:它表明剪贴板的概念可以以这样一种方式进行抽象,以便在广泛的操作系统上实现。作为一个具体的例子,Qt 的剪贴板使用 MIME 类型来交流打字。如果 .NET Core 有一个剪贴板,那么它将成为移植过程的一部分,以实现访问剪贴板的平台细节到 .NET Core 呈现的任何抽象。【参考方案6】:

亡灵术。 人们似乎很难弄清楚如何在 Linux 上使用剪贴板。

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

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

这是 Klipper DBus 接口的代码(对于 *** 来说有点大):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,只要您不想在同步上下文中使用获取/设置剪贴板内容。

【讨论】:

明确地说,您是说必须安装 GTK 或 Klipper 是可能不存在的命令行工具的 更好 替代品,或者这些只是替代品?我认为您现在的回答实际上是说“不要依赖默认情况下[不一定]安装的命令行工具[您应该依赖默认情况下也不一定安装的这些其他工具]”。跨度> @Matt Thomas:嗯,在某种程度上。 D-Bus 默认安装(几乎在任何地方)。如果您使用 KDE,则默认安装 Klipper,如果您使用 Gnome,则默认安装 GTK。在这两种情况下,默认情况下都不会安装 xclip。所以我实际上想说的是:检查 dbus&klipper 是否存在,如果不存在,请尝试 GTK#,如果不存在,则可以选择回退到 xclip。那将是非常正确的方法,恕我直言。比只调用 xclip 更好(而且速度更快)。我什至会说:Gnome 是垃圾,使用它的人只能怪自己。所以只需使用 klipper 就可以了。 @Matt Thomas:毕竟,如果你运行 Gnome,你可以只安装 klipper,这和安装 xclip 一样快。但我想我真正想说的是,启动一个新进程缓慢且效率低下,此外 WriteAllText/ReadAllText 和 xclip 容易出现文本编码错误。但是,只要您保持在英文字母范围内,并且如果您具有安装 xclip 的管理员权限,或者具有修复路径环境变量并将其破解的意愿和技能,它就可以工作。

以上是关于.Net Core - 复制到剪贴板?的主要内容,如果未能解决你的问题,请参考以下文章

如何将 RTF 文本复制到剪贴板以在 excel 中使用

如何在没有 Flash 的情况下在 Firefox 中实现复制到剪贴板。需要实施[重复]

CAD无法复制到剪贴板怎么解决?

复制内容到剪贴板

autoit中如何把指定内容复制到剪贴板?

jquery复制文本到windows剪贴板的方法,jquery copy.js插件怎么用。