.Net Core - 复制到剪贴板?

.Net Core - copy to clipboard?

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

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

编辑:不幸的是,直到现在我的问题和人们在阅读问题时听到的内容之间存在差异。根据评论和答案,有两件事很清楚。首先,很少有人关心是否存在最真实的“象牙塔”平台不可知论。其次,当人们 post 代码示例显示您如何在不同平台上使用剪贴板时,技术上正确的答案(“”)令人困惑。所以我划掉了附加条款。

剪贴板 class 不见了,希望在不久的将来能增加一个选项。当它发生时...您可以 运行 一个带有 ProcessStartInfo 的本机 shell 命令。

我是 Net Core 的菜鸟,但创建此代码以在 Windows 和 Mac 上发送并发送到剪贴板:

OS检测Class

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;
    }
}

剪贴板Class

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

由于我还不能发表评论,我将 post 这个作为答案,尽管它实际上只是 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.

的检查

搭在 Erik's comment to the OP above 的尾巴上:

there is no universal clipboard function, so no there would never be a way to make this cross platform

他完全正确。所以 technically-correct 答案是:

不,完全platform-agnostic是不可能的。

正如他所说,剪贴板基本上是一个 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

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

对于它的价值,只需在终端 window 中 right-clicking 并从那里选择“复制”对我来说效果很好。对于那些需要严格 long-term 解决方案的程序,我使用正常的进程间通信。

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

  • Windows 使用 .NET Framework 4.6.1 及更高版本
  • Windows 使用 .NET Core 2.0 及更高版本
  • Windows 使用 Mono 5.0 及更高版本
  • OSX 使用 .NET Core 2.0 及更高版本
  • OSX 使用 Mono 5.20.1 及更高版本
  • Linux 使用 .NET Core 2.0 及更高版本
  • Linux 使用 Mono 5.20.1 及更高版本

用法:

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);
        }
    }
}

死灵法术。
人们似乎无法弄清楚如何在 Linux 上使用剪贴板。

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

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

这是 Klipper DBus 接口的代码(对于 Whosebug 来说有点大):
https://pastebin.com/HDsRs5aG

和摘要class:
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 需要摘要 class 才能在 get/set 属性 中同步。 实际剪贴板处理不需要 AsyncEx,只要您不想在同步上下文中使用 get/set 剪贴板内容。

我也在找同样的东西。 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-Clipboard and Set-Clipboard 似乎在不同版本的 PowerShell 中突然出现和消失。它们在 5.1 中可用,在 6 中不可用,但在 7 中又回来了。