Как загрузить изображения в фоновом режиме?

Я пытаюсь загрузить изображение в фоновом режиме, а затем обновить пользовательский интерфейс. Я играл с этим весь день и не знаю, что мне не хватает. Я получаю следующую ошибку:

«Вызывающий поток не может получить доступ к этому объекту, потому что он принадлежит другому потоку».

Я искал следующий пример за примером, но, похоже, не нашел ответа. Я также обернул код, касающийся пользовательского интерфейса, в другой BeginInvoke.

Обновление 3: мораль истории. ImageSource не является потокобезопасным для доступа.

Обновление 2: это должно быть простое решение :). Я попытался выполнить клонирование, но это не привело к успеху, но я получил другую ошибку: «Целью вызова было создано исключение».

Обновление 1. Я попробовал BackgroundWorker, но все равно получаю ту же ошибку, но она возникает на brush.ImageSource.Height. Правильно ли я сигнализирую пользовательскому интерфейсу? Какие-либо предложения?

Вот мой XAML:

<Window x:Class="Slideshow.Show"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <DockPanel>
        <Canvas Background="Black" Name="canvas">
            <Viewbox Name="background" Stretch="Uniform">
                <Rectangle name="background" />
            </Viewbox>
        </Canvas>
    </DockPanel>
</Window>

Вот часть кода:

namespace Slideshow
{
    public class Show 
    {
        public Show()
        {
            BackgroundWorker bw = new BackgroundWorker();
            bw.DoWork += new DoWorkEventHandler(bw_DoWork);
            bw.RunWorkerCompleted += 
                new RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);
            bw.RunWorkerAsync();
        }

        void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            BitmapSource bitmap = e.Result as BitmapSource;

            if (bitmap != null)
            {
                this.Dispatcher.BeginInvoke(DispatcherPriority.Normal 
                    (ThreadStart)delegate()
                {
                    Image image = new Image();
                    image.Source = bitmap;
                    background.Child = image;
                 });
            }
        }

        void bw_DoWork(object sender, DoWorkEventArgs e)
        {
            BitmapSource bitmapSource = CreateBrush(GetRandomFile());
            e.Result = bitmapSource;
         }
    }
}

person kevindaub    schedule 04.04.2009    source источник
comment
Попробуйте удалить часть Dispatcher.BeingInvoke. Весь смысл BackgroundWorker в том, что событие автоматически маршалируется обратно в поток пользовательского интерфейса, поэтому вам не о чем беспокоиться.   -  person Samuel    schedule 04.04.2009
comment
Проблема заключалась в том, что ImageSource не гарантированно был потокобезопасным, поэтому все могло умереть. Как только я заменил это на BitmapSource, моя проблема была решена!   -  person kevindaub    schedule 04.04.2009


Ответы (5)


Я обычно использую ThreadPool.QueueUserWorkItem, чтобы загружаю изображение, а затем, когда операция завершается, я снова вызываю поток пользовательского интерфейса, используя потокобезопасный объект _ 2_. Источник изображения не является потокобезопасным, вам придется использовать что-то вроде JpegBitmapDecoder, также есть PngBitmapDecoder.

Например:

public Window()
{
    InitializeComponent();

    ThreadPool.QueueUserWorkItem(LoadImage,
         "http://z.about.com/d/animatedtv/1/0/1/m/simp2006_HomerArmsCrossed_f.jpg");
}

public void LoadImage(object uri)
{
    var decoder = new JpegBitmapDecoder(new Uri(uri.ToString()), BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
    decoder.Frames[0].Freeze();
    this.Dispatcher.Invoke(DispatcherPriority.Send, new Action<ImageSource>(SetImage), decoder.Frames[0]);
}

public void SetImage(ImageSource source)
{
    this.BackgroundImage.Source = source;
}
person bendewey    schedule 04.04.2009
comment
МОЙ БОГ! Я был настолько разочарован ImageSource, что никогда не замечал в документации, что он не является Thread Safe. - person kevindaub; 04.04.2009
comment
Вот и все. Точно так же, как Forms / WPF - это только STA, за исключением того, что это не так хорошо документировано. - person Samuel; 04.04.2009

Еще одна вещь, которая есть в нашем проекте. Поскольку ImageSource помещается в пользовательский интерфейс, вы должны проверить, не заморожен ли он:

public void SetImage(ImageSource source)
{
   ImageSource src = null;
   if(!source.IsFrozen)
       src = source.GetAsFrozen();
   else
       src = source; 
   this.BackgroundImage.Source = src;
}
person Methos    schedule 08.04.2009
comment
Я думаю, что столкнулся с проблемой, что вызов IsFrozen не был потокобезопасным. AKA, если он не был заморожен, я получил ошибку. Спасибо за внимание. - person kevindaub; 08.04.2009
comment
Я обновил метод SetImage, который используется в примере Бендевея. Конечно, его нужно вызывать с помощью Dispatcher.Invoke. И перед вызовом Dispatcher.Invoke вам лучше вызвать Dispatcher.CheckAccess, который вернет true, если требуется диспетчеризация, и false, если вы можете обновить пользовательский интерфейс без Dispatcher. - person Methos; 08.04.2009

Вы хотите включить несколько потоков в свое приложение WPF. Как указано в статье ниже, WPF заставляет вас выполнять всю работу пользовательского интерфейса в потоке, создавшем пользовательский интерфейс.

Проверьте это: http://pjbelfield.wordpress.com/2007/10/29/worker-threads-and-the-dispatcher-in-wpf/

и для интенсивной работы UI:

http://eprystupa.wordpress.com/2008/07/28/running-wpf-application-with-multiple-ui-threads/.

person Joshua    schedule 04.04.2009

Ваш код для загрузки изображения (.jpg) в память должен выполняться в фоновом режиме. Код для загрузки изображения из памяти в элемент управления WPF должен выполняться в обычном потоке UI WPF. В фоновый поток следует помещать только медленные / длительные задачи. Как только длинная задача завершена, она должна сигнализировать потоку пользовательского интерфейса, чтобы обновить представление с результатом длинной задачи.

Мой любимый способ - использовать класс BackgroundWorker:

var bg = new System.ComponentModel.BackgroundWorker();
bg.DoWork += Work_Function;
bg.RunWorkerCompleted += UI_Function;
bg.RunWorkerAsync();

void Work_Function(object sender, DoWorkEventArgs e) { ... }
void UI_Function(object sender, RunWorkerCompletedEventArgs e) { ... }

Когда вы вызываете RunWorkerAsync (), сначала вызывается ваша Work_Function (). По завершении вызывается UI_Function (). Рабочая функция может помещать одну переменную в свойство DoWorkEventArgs.Result, которое доступно из свойства RunWorkerCompletedEventArgs.Result.

Думаю, вам также может быть интересна эта статья: http://www.codeproject.com/KB/WPF/WPF_Explorer_Tree.aspx Здесь показано, как настроить отложенную загрузку в виде дерева в WPF. Я думаю, что основная концепция (добавить фиктивный узел, перехватить отображение фиктивного узла, загрузив вместо этого следующее изображение) применима к вашей работе.

person vanja.    schedule 04.04.2009
comment
Я обновил свой ответ, чтобы показать пример того, как это можно сделать. - person vanja.; 04.04.2009
comment
Спасибо. Я попробую. - person kevindaub; 04.04.2009
comment
Я попробовал Background Worker, но безуспешно. Та же проблема. :( - person kevindaub; 04.04.2009

Ошибка, которую вы сейчас получаете, - хорошая новость, это означает, что BackgroundWorker работает. Вы можете попробовать клонировать его в RunWorkerCompleted с помощью brush.Clone();.

Попробуйте удалить часть Dispatcher.BeingInvoke. Весь смысл BackgroundWorker в том, что событие автоматически маршалируется обратно в поток пользовательского интерфейса, поэтому вам не о чем беспокоиться.

person Samuel    schedule 04.04.2009
comment
К сожалению, клонирование не сработало, но я получаю другую ошибку: цель вызова сгенерировала исключение. - person kevindaub; 04.04.2009