如何在控制台应用程序中正确使用剪贴板?

How do I properly use Clipboard in a console application?

我是 C# 初学者,我正在尝试创建一个 Windows 服务(带 Topshelf 的控制台应用程序)(.Net Framwork 4.8),每秒获取和设置剪贴板(是的,无用的服务,它是仅供学习)。

在我的服务 class 中使用 System.Windows.Forms 作为参考时,计时器 class 停止工作(“'Timer' 是 'System.Windows.Forms.Timer' 和 'System.Timers.Timer'") 并且应用程序在我使用剪贴板 class 的行抛出 System.Threading.ThreadStateException:“当前线程必须设置为单线程单元 (STA) 模式才能调用 OLE制作完成后,请确保您的 Main 函数上标有 STAThreadAttribute。

using System.Timers;
using System.Windows.Forms;

namespace ClipboardProject
{
    public class TimerClipboard
    {
        private readonly Timer _timer;

        public TimerClipboard()
        {
            _timer = new Timer(1000) { AutoReset = true };
            _timer.Elapsed += TimerElapsed;
        }

        private void TimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            string userClipboard = Clipboard.GetText();
            Clipboard.SetText($"Latest copy: {userClipboard}");
        }

        public void Start()
        {
            _timer.Start();
        }

        public void Stop()
        {
            _timer.Stop();
        }
    }
}

我做错了什么?

已编辑:

这是我的主要方法。

using System;
using Topshelf;

namespace ClipboardProject
{
    public class Program
    {
        static void Main(string[] args)
        {
            var exitCode = HostFactory.Run(x =>
             {
                 x.Service<TimerClipboard>(s =>
                 {
                     s.ConstructUsing(timerClipboard=> new TimerClipboard());
                     s.WhenStarted(timerClipboard=> timerClipboard.Start());
                     s.WhenStopped(timerClipboard=> timerClipboard.Stop());
                 });

                 x.RunAsLocalSystem();
                 x.SetServiceName("Random ServiceName");
                 x.SetDisplayName("Random DisplayName");
                 x.SetDescription("Random Description");
             });

             int exitCodeValue = (int)Convert.ChangeType(exitCode, exitCode.GetTypeCode());
             Environment.ExitCode = exitCodeValue;
        }
    }
}

System.Timers.Timer.Elapsed 处理程序总是在后台线程中 运行,因此 Clipboard OLE 调用导致 System.Threading.ThreadStateException.

您可以通过创建新的 System.Threading.Thread 来处理它,并通过 SetApartmentStart(ApartmentState.STA) 强制它作为 STA 线程启动,但这是 低效的 可怕的错误的解决方案:

private void TimerElapsed(object sender, ElapsedEventArgs e)
{
    System.Threading.Thread t = new System.Threading.Thread(() =>
    {
        string userClipboard = Clipboard.GetText();
        Clipboard.SetText($"Latest copy: {userClipboard}");
    });

    t.SetApartmentState(System.Threading.ApartmentState.STA);
    t.Start();
}

因为在每个间隔刻度上它都会创建新的 ThreadThreads 会带来巨大的性能成本,尤其是在循环中创建时。

因此,正确的解决方案可能是使用 PresentationFramework 库中的 System.Windows.Threading.DispatcherTimer

public class TimerClipboard
{
    private readonly System.Windows.Threading.DispatcherTimer _timer;

    public TimerClipboard()
    {
        _timer = new System.Windows.Threading.DispatcherTimer();
        _timer.Interval = TimeSpan.FromSeconds(1);
        _timer.Tick += OnDispatcherTimerTick;
    }

    private void OnDispatcherTimerTick(object sender, EventArgs e)
    {
        string userClipboard = Clipboard.GetText();
        Clipboard.SetText($"Latest copy: {userClipboard}");
    }

    public void Start() => _timer.Start();
    public void Stop() => _timer.Stop();
}

关于 "'Timer' 是 System.Windows.Forms.TimerSystem.Timers.Timer" 之间的模糊引用:那是因为两个命名空间都有 class Timer,因此您的 Studio 不知道您想要和需要使用哪一个。可以通过删除另一个 using 或明确指定来解决:

using Timer = System.Timers.Timer;
// or
using Timer = System.Windows.Forms.Timer;

namespace ClipboardProject
{
    // ...
}

编辑。

正如@Hans Passant 所注意到的,DispatcherTimer.Tick 事件在没有调度程序的情况下无法触发,因此上面的解决方案不适用于 Topshelf。所以我提议重写 TimerClipboard class 以从那里删除任何计时器并使用简单的基于标志的 while 循环。由于没有计时器,我将其重命名为 ClipboardWorker.

完整的解决方案如下所示:

using System;
using System.Threading.Tasks;
using System.Windows.Forms;
using Topshelf;

namespace ClipboardProject
{
    class Program
    {
        // Add STA attribute to Main method
        [STAThread] 
        static void Main(string[] args)
        {
            var topshelfExitCode = HostFactory.Run(x =>
            {
                x.Service<ClipboardWorker>(s =>
                {
                    s.ConstructUsing(cw => new ClipboardWorker());
                    s.WhenStarted(cw => cw.Start());
                    s.WhenStopped(cw => cw.Stop());
                });

                x.RunAsLocalSystem();
                x.SetServiceName("ClipboardWorkerServiceName");
                x.SetDisplayName("ClipboardWorkerDisplayName");
                x.SetDescription("ClipboardWorkerDescription");
            });

            Environment.ExitCode = (int)topshelfExitCode;
        }
    }

    public class ClipboardWorker
    {
        // Flag that would indicate our Worker in running or not
        private bool _isRunning;
        private int _interval = 1000; // Default value would be 1000 ms

        public bool IsRunning { get => _isRunning; }
        public int Interval 
        { 
            get => _interval;
            // Check value which sets is greater than 0. Elseway set default 1000 ms
            set => _interval = value > 0 ? value : 1000;
        }

        // Constructor
        public ClipboardWorker() 
        {
            Console.WriteLine();
            Console.WriteLine("ClipboardWorker initialized.");
        }

        // "Tick" simulation. 
        private void DoWorkWithClipboard()
        {
            // Loop runs until Stop method would be called, which would set _isRunning to false
            while (_isRunning)
            {
                Console.WriteLine(); // <--- just line break for readability

                string userClipboard = Clipboard.GetText();
                Console.WriteLine($"Captured from Clipboard value: {userClipboard}");

                Clipboard.SetText($"Latest copy: {userClipboard}");
                Console.WriteLine($"Latest copy: {userClipboard}");

                // Use delay as interval between "ticks"
                Task.Delay(Interval).Wait();               
            }
        }

        public void Start()
        {
            // Set to true so while loop in DoWorkWithClipboard method be able to run
            _isRunning = true;
            Console.WriteLine("ClipboardWorker started.");
            // Run "ticking"
            DoWorkWithClipboard();          
        }

        public void Stop()
        {
            // Set to false to break while loop in DoWorkWithClipboard method
            _isRunning = false;
            Console.WriteLine("ClipboardWorker stopped.");
        }
    }
}

示例输出: