Deniz Cetinalp
Deniz Cetinalp

Reputation: 911

How to render a visual on another thread

Im trying to render a canvas on another thread. Here is my attempt:

// returns path to exported image
private string exportToImage(double width, double height, Visual visual)
{
    var filename = string.Format(@"{0}.png", Guid.NewGuid());
    var tempFile = Path.Combine(tempDir, filename);
    Rect rect = new Rect(0, 0, width, height);
    RenderTargetBitmap rtb = new RenderTargetBitmap((int)rect.Right,
        (int)rect.Bottom, 96d, 96d, System.Windows.Media.PixelFormats.Default);

    Thread RENDER_THREAD = new Thread(() => 
    {
        this.Dispatcher.Invoke((Action)(() =>
        {
            rtb.Render(visual);
        }));
    });
    RENDER_THREAD.Start();

    //endcode as PNG
    BitmapEncoder pngEncoder = new PngBitmapEncoder();
    pngEncoder.Frames.Add(BitmapFrame.Create(rtb));

    //save to memory stream
    System.IO.MemoryStream ms = new System.IO.MemoryStream();

    pngEncoder.Save(ms);
    ms.Close();
    System.IO.File.WriteAllBytes(tempFile, ms.ToArray());
    return tempFile;
}

Im not getting any errors but the rendered result is an empty image. Is the instance of rtb in the main thread and the new thread the same?

Edit: Here is my latest attempt.

I create the thread on a MouseUp event:

var curLayer = GetItemsPanel((canvasDataBinding.ItemContainerGenerator.ContainerFromIndex(Binding_LayersListView.SelectedIndex)));
Thread RASTERIZE_THREAD = new Thread(() => { exportToImage(curLayer.ActualWidth, curLayer.ActualHeight, curLayer); });
RASTERIZE_THREAD.Start();

and here is my method the new thread uses:

void exportToImage(double width, double height, Visual visual)
{
    var filename = string.Format(@"{0}.png", Guid.NewGuid());
    var tempFile = Path.Combine(tempDir, filename);
    Rect rect = new Rect(0, 0, width, height);
    RenderTargetBitmap rtb = new RenderTargetBitmap((int)rect.Right,
        (int)rect.Bottom, 96d, 96d, System.Windows.Media.PixelFormats.Default);

    this.Dispatcher.Invoke(new Action(() =>
    {
        rtb.Render(visual);
    }));

    //endcode as PNG
    BitmapEncoder pngEncoder = new PngBitmapEncoder();
    pngEncoder.Frames.Add(BitmapFrame.Create(rtb));

    //save to memory stream
    System.IO.MemoryStream ms = new System.IO.MemoryStream();

    pngEncoder.Save(ms);
    ms.Close();
    System.IO.File.WriteAllBytes(tempFile, ms.ToArray());
}

So why does it tell me that The calling thread cannot access this object because a different thread owns it on rtb.Render(visual)? Im not calling exportToImage from anywhere else, so why is the Dispatcher not associated with the thread I created?

Edit: I needed to create the RebderTargetBitmap inside Dispatcher.Invoke().

Upvotes: 3

Views: 2881

Answers (1)

pushpraj
pushpraj

Reputation: 13659

here is a code how you can render the same in a thread and make it async

whole idea is

  • serialize the visual into xaml string
  • create a STA Thread
  • then let thread deserialize the same.
  • host the deserialized visual in a content control, and do Arrange etc.
  • last but not the least, render it (taken from your code)

this code in not dependent on Dispatcher so it is able to run full async

    void exportToImage(double width, double height, Visual visual)
    {
        //this line can not be inside a thread because different thread owns the visual
        string visualData = XamlWriter.Save(visual);

        Thread t = new Thread(() =>
            {
                var filename = string.Format(@"{0}.png", Guid.NewGuid());
                var tempFile = System.IO.Path.Combine("c:\\", filename);

                Rect rect = new Rect(0, 0, width, height);

                //create a host control to host the visual
                ContentControl cc = new ContentControl();
                cc.Content = XamlReader.Parse(visualData);
                //call Arrange to let control perform layout (important)
                cc.Arrange(rect);

                RenderTargetBitmap rtb = new RenderTargetBitmap((int)rect.Right,
                    (int)rect.Bottom, 96d, 96d, System.Windows.Media.PixelFormats.Default);

                rtb.Render(cc);

                //endcode as PNG
                BitmapEncoder pngEncoder = new PngBitmapEncoder();
                pngEncoder.Frames.Add(BitmapFrame.Create(rtb));

                //save to memory stream
                System.IO.MemoryStream ms = new System.IO.MemoryStream();

                pngEncoder.Save(ms);
                ms.Close();
                System.IO.File.WriteAllBytes(tempFile, ms.ToArray());
            });
        //STA is necessary for XamlReader.Parse, and proper rendering
        t.SetApartmentState(ApartmentState.STA);
        t.Start();
    }

I have successfully tested this code to render some test visuals. but results may vary depending on how you use. as a guess bindings & raster images may have some issues, however in my tests it worked like a charm.

Upvotes: 5

Related Questions