MxLDevs
MxLDevs

Reputation: 19546

Using a thread to copy image to clipboard and managing the thread

I have an application that holds an image in a picture box. When you press ctrl+C it copies the image to clipboard. I use a thread to perform the actual clipboard operations.

protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
    if (keyData == (Keys.Control | Keys.C))
    {
        clipboardThread = new Thread(copy_to_clipboard);
        clipboardThread.SetApartmentState(ApartmentState.STA);
        clipboardThread.Start();
        return true;
    }
    return base.ProcessCmdKey(ref msg, keyData);
}

private void copy_to_clipboard()
{
    if (pic_display.Image != null)
    {
        using (MemoryStream stream = new MemoryStream())
        {
            clipboardStatus.Text = "Copying image to clipboard...";
            pic_display.Image.Save(stream, ImageFormat.Png);
            var data = new DataObject("PNG", stream);
            Clipboard.Clear();
            Clipboard.SetDataObject(data, true);
            clipboardStatus.Text = "Copied successfully!";
        }
    }
}

Right now, if I repeatly send ctrl+C (eg: hold the keys down), a new thread would be spawned. How can I change the code so that I re-use the clipboard thread, and have it tell me whether it's currently copying data or not so that I don't try to execute another copy command while it's still working.

UPDATE

Now it works

I've changed it to use a background worker

protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
    if (keyData == (Keys.Control | Keys.C))
    {
        if (!backgroundWorker1.IsBusy)
            backgroundWorker1.RunWorkerAsync();
        return true;
    }
    return base.ProcessCmdKey(ref msg, keyData);
}

private void copy_to_clipboard()
{
    using (var stream = new MemoryStream())
    {
        clipboardStatus.Text = "Copying image to clipboard...";
        pic_display.Invoke((Action)(() => {
            if (pic_display.Image != null)
              pic_display.Image.Save(stream, ImageFormat.Png); 
        }));
        if (stream.Position == 0) return; // No image was saved
        var data = new DataObject("PNG", stream);
        BeginInvoke ( (Action) ( ()=> {
            Clipboard.Clear();
            Clipboard.SetDataObject(data, true);
        }
        clipboardStatus.Text = "Copied successfully!";
    }
}

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    copy_to_clipboard();
}

But now an exception occurs on

Clipboard.Clear();

saying

Current thread must be set to single thread apartment (STA) mode before OLE calls can be made. Ensure that your Main function has STAThreadAttribute marked on it.

Upvotes: 0

Views: 4428

Answers (4)

Peter Ritchie
Peter Ritchie

Reputation: 35869

I would recommend doing the Save during the click--you should be doing it on the UI thread anyway. You could use InvokeRequired/Invoke; but then you don't have any control over when that Save is going to take over the UI thread. It could be a noticeable amount of time after the Click and be disconcerting to the user. Or, it could be after the user changes the image. It sucks that it will take any time away from the UI thread; but the closer to the Click the better from a usability standpoint. In which case, maybe something like:

protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
    if (keyData == (Keys.Control | Keys.C))
    {
        if (pic_display.Image != null)
        {
            MemoryStream stream = new MemoryStream())
            clipboardStatus.Text = "Copying image to clipboard...";
            pic_display.Image.Save(stream, ImageFormat.Png);

            clipboardThread = new Thread(copy_to_clipboard);
            clipboardThread.SetApartmentState(ApartmentState.STA);
            clipboardThread.Start(stream);
        }
        return true;
    }
    return base.ProcessCmdKey(ref msg, keyData);
}

private void copy_to_clipboard(object state)
{
    var stream = (Stream) state;
    try
    {
        var data = new DataObject("PNG", stream);
        Clipboard.Clear();
        Clipboard.SetDataObject(data, true);
        BeginInvoke((MethodInvoker) (() => clipboardStatus.Text = "Copied successfully!"));
    }
    finally
    {
        stream.Dispose();
    }
}

Upvotes: 1

Chris Shain
Chris Shain

Reputation: 51359

First off, you should be invoking when accessing UI controls from a background thread:

private void copy_to_clipboard()
{
    using (var stream = new MemoryStream())
    {
        clipboardStatus.Text = "Copying image to clipboard...";
        pic_display.Invoke((Action)()=> { 
            if (pic_display.Image != null)
              pic_display.Image.Save(stream, ImageFormat.Png); 
        });
        if (stream.Position == 0) return; // No image was saved
        var data = new DataObject("PNG", stream);
        Clipboard.Clear();
        Clipboard.SetDataObject(data, true);
        clipboardStatus.Text = "Copied successfully!";
    }
}

Then invoke that from a BackgroundWorker. Lots of examples of that are available on the web.

Upvotes: 1

Cashley
Cashley

Reputation: 516

Can't you just store the thread as a member? Unless I'm misunderstanding the question, Thread.IsAlive will do what you want. However if you are modifying UI controls as a poster above said you should really use Invoke to make sure this happens on the UI thread.

protected Thread clipboardThread;

protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{

if (keyData == (Keys.Control | Keys.C))
{
    if (clipBoardThread == null)
    {
       clipboardThread = new Thread(copy_to_clipboard);
       clipboardThread.SetApartmentState(ApartmentState.STA);
       clipboardThread.IsBackGround = false;
    }
    if (!clipboardThread.IsAlive)
    {
       clipboardThread.Start();
    }
    return true;
}
return base.ProcessCmdKey(ref msg, keyData);
}

private void copy_to_clipboard()
{
if (pic_display.Image != null)
{
    using (MemoryStream stream = new MemoryStream())
    {
        clipboardStatus.Text = "Copying image to clipboard...";
        pic_display.Image.Save(stream, ImageFormat.Png);
        var data = new DataObject("PNG", stream);
        Clipboard.Clear();
        Clipboard.SetDataObject(data, true);
        clipboardStatus.Text = "Copied successfully!";
    }
}
}

Upvotes: 0

kmatyaszek
kmatyaszek

Reputation: 19296

Try something like that:

object lockObj = new object();

        private void copy_to_clipboard()
        {
            lock (lockObj)
            {
                if (pictureBox1.Image != null)
                    {
                        using (MemoryStream stream = new MemoryStream())
                        {
                            clipboardStatus.Text = "Copying image to clipboard...";                            
                            pictureBox1.Image.Save(stream, ImageFormat.Png);
                            var data = new DataObject("PNG", stream);
                            Clipboard.Clear();
                            Clipboard.SetDataObject(data, true);                            
                            clipboardStatus.Text = "Copied successfully!";
                        }
                    } 
            }
        }

Upvotes: 0

Related Questions