locked
Компонент в видимой области (c#, WPF, VS 2010-2012) RRS feed

  • Вопрос

  • Здравствуйте.
    Столкнулся со следующей проблемкой.

    Во WrapPanel в главном окне загружается список из множества объектов UserControl одного типа. В методе Loaded этого UserControl прописан асинхронный метод, загружающий изображений в компонент Image. Метод этот довольно долгий, а изображения довольно большие. В следствии этого, при загрузке около 100 таких компонентов выскакивает исключение о переполнении памяти. Подумал решить эту проблему очисткой памяти после загрузки изображения (в самом UserControl после загрузки изображения вызываю GC.Collect() ). К сожалению это не решило моих проблем.

    В Windows есть интересная особенность. Если открыть папку и множеством изображений с видом "эскизы" - загружаются эскизы только тех файлов, которые я сейчас вижу в папке, а остальные, только если прокрутить до них.

    Можно ли такое организовать в своем приложении, и если да то как?
    Т.е. чтобы загрузка изображений происходила только в тех UserControl, которые в данный момент видны на экране. Это очень сильно бы разгрузило само приложение.

    Заранее благодарю, Олег.

    15 марта 2013 г. 5:18

Ответы

  • Свою задачу - узнать виден ли компонент - решил:

    private void ImagesListScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
            {
                if ((UnloadedImagesList != null) && (UnloadedImagesList.Count > 0))
                    foreach (ImagesListItem ILI in UnloadedImagesList) ILI.CheckVisible();
            }
    
    
    public async void CheckVisible()
            {
                int itemY = (int)VisualTreeHelper.GetOffset(this).Y;
                int listY = (int)(VisualTreeHelper.GetOffset((this.Parent as WrapPanel)).Y * (-1));
                int scrollH = (int)((this.Parent as WrapPanel).Parent as ScrollViewer).ActualHeight;
                bool show_in_down = ((itemY - listY) < scrollH);
                bool show_in_up = ((itemY + this.ActualHeight) > listY);
                if (show_in_up && show_in_down)
                {
                    ImageSource imgsource = await System.Threading.Tasks.Task.Run(() => { return new System.Drawing.Bitmap(ItemData.FileName).InNewSize(new ImageSize(90, 90), System.Drawing.Drawing2D.InterpolationMode.Low).ToBitmapImage(); });
                    GC.Collect();
                    Image.Source = imgsource;
                    UnloadedImagesList.Remove(this);
                }
            }

    Метод все же считаю костарным - но работает )))

    При загрузке в список более 2000 изображений с фотика (~3000*4000 точек) объем памяти не увеличивается выше 240 Мб, а по окончании загрузки падает до ~20 Мб.

    Использовал я UserControl. Интересно, конечно, глянуть на реализацию такого списка без него.

    • Помечено в качестве ответа Hovanskiy 15 марта 2013 г. 18:06
    15 марта 2013 г. 11:29
  • Проблема решена.

    Заменил BindingList на свой класс на его основе с переопределением метода Add:

    private async Task SearchImagesFiles(System.Windows.Forms.FolderBrowserDialog FBD)
            {
                await Task.Run(() =>
                {
                    Action<string> addInListAction = delegate(string findFile)
                    {
                        imageItems.Items.Add(new ImageItem() { Path = findFile, Title = new FileInfo(findFile).Name });
                        GC.Collect();
                    };
                    return SearchFilesInDirectory(FBD.SelectedPath, new List<string>() { "*.jpeg", "*.jpg", "*.png", "*.gif", "*.bmp" }, addInListAction);
                });
            }
    
    public class ImageItems
        {
            public ImageItems()
            {
                Items = new ImageItemList();
            }
            public void OnPropertyChenged(string PropertyName)
            {
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(PropertyName));
                }
            }
            public event PropertyChangedEventHandler PropertyChanged;
            public delegate void PropertyChangedEventHandler(object sender, System.ComponentModel.PropertyChangedEventArgs e);
            private ImageItemList items;
            public ImageItemList Items
            {
                get { return items; }
                set
                {
                    items = value;
                    OnPropertyChenged("Items");
                }
            }
        }
        public class ImageItemList : BindingList<ImageItem>
        {
            public new void Add(ImageItem item)
            {
                if (this.Items.Count(x => x.Path == item.Path) == 0)
                    base.Add(item);
            }
        }

    • Помечено в качестве ответа Hovanskiy 16 марта 2013 г. 11:07
    16 марта 2013 г. 11:07
  • Блин. Час себе голову ломал, не мог сделать! Написал здесь и за  минут решил!?:):):)

    Решение удаления множества элементов из ListBox:

    List<ImageItem> remList = new List<ImageItem>();
    foreach (ImageItem ii in ListBox.SelectedItems)
        remList.Add(ii);
    foreach (ImageItem item in remList)
        ((ImageItemList)ListBox.ItemsSource).Remove(item);
    remList = null;

    • Помечено в качестве ответа Hovanskiy 16 марта 2013 г. 19:52
    16 марта 2013 г. 19:52

Все ответы

  • Судя по всему проблема вызвана не правильным подходом к решению задачи. Во первых меня смущает использование UserControl и WrapPanel  вместо DataTemplate и ItemsControl. Во вторых если вам нужны только превью, то стоит позаботится о декодировании фото из оригинального размера к размеру превьюшки.

    В общем можно дать конкретные рекомендации, если вы опишите концепцию. То есть что делает приложение? Тогда вам подскажут как это сделать с повышением производительности


    Женат на WPF. Тайно встречаюсь с WinRT. Не сложилось с C#!

    15 марта 2013 г. 5:34
  • Загрузка изображений в Image так и происходит.
    Я изменяю размер исходного изображения до пределов в 100*100 пикселей и результат гружу в Image. Но ведь чтобы изменить размер изображения, нужно его сначала полностью загрузить, а так как у меня метод этот асинхронный, то и получается, что: 1) в память одновременно загружается множество изображений; 2) происходит расчет новых размеров изображений; 3) происходит создание нового изображения нужных размеров; 4) происходит преобразование полученного изображения из System.Drawing.Bitmap в System.Windows.Media.BitmapImage; 5) происходит загрузка изображения в Image.

    По поводу DataTemplate и ItemsControl. Никогда не имел с ними дел, сейчас, конечно посмотрю, что они из себя представляют.
    На UserControl помимо Image есть еще компоненты, в которые тоже грузиться информация.

    А сказать компоненту что он виден или не виден - было бы весьма полезно в разных ситуациях.

    15 марта 2013 г. 6:04
  • DataTemplate и ItemsControl отпадают. Ни в ListBox ни в ListView нельзя сделать отображение в виде плиток. WrapPanel мне идеально подходит.
    15 марта 2013 г. 6:20
  • DataTemplate и ItemsControl отпадают. Ни в ListBox ни в ListView нельзя сделать отображение в виде плиток. WrapPanel мне идеально подходит.

    Ваш вопрос относится все таки к WPF или WinRT? А то в заголовке пишите WPF, а вопрос создан в ветке для WinRT.

    Итак если речь о WPF, то ListBox поддерживает любые контейнеры в том числе и WrapPanel. Скорее всего это актуально и в WinRT, но я не пробовал.


    Женат на WPF. Тайно встречаюсь с WinRT. Не сложилось с C#!

    15 марта 2013 г. 6:33
  • Вопрос по WPF.

    Проблему, в принципе, решил следующим образом (на примере Button'ов)

    Объявляю класс своего контрола:

    class MyButton : Button
    {
        public bool CheckVisible()
        {
            bool IisVisible = true;
            if (VisualTreeHelper.GetOffset(this).Y > ((Parent as WrapPanel).Parent as ScrollViewer).ActualHeight)
                IisVisible = false;
            if (VisualTreeHelper.GetOffset(this).Y + this.ActualHeight < (VisualTreeHelper.GetOffset((Parent as WrapPanel)).Y * (-1)))
                IisVisible = false;
            return IisVisible;
        }
    }

    Каждый такой контрол сожаю во WrapPanel, который расположен в ScrollView.

    Обрабатываю событие ScrollChanged ScrollView:

            private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
            {
                foreach (MyButton mbt in ((sender as ScrollViewer).Content as WrapPanel).Children) mbt.CheckVisible();
            }

    Но данное решение я, мягко говоря, считаю кустарным.

    Может кто-нибудь что-то подскажет!?

     
    • Изменено Hovanskiy 15 марта 2013 г. 7:37
    15 марта 2013 г. 7:29
  • Может кто-нибудь что-то подскажет!?  

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

    Не важно сколько данных отображает UserControl. Если задача только отображать набор данных, то применение UserControl в корне не верный шаг в том числе с точки зрения производительности.

    Далее WrapPanel это всего лишь контейнер лишенный ряда полезностей для коллекций, коем является ваш набор UserControl'ов. А вот ItemsControl c WrapPanel в качестве панели имеет все плюсы в том числе и в вопросе производительности.

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


    Женат на WPF. Тайно встречаюсь с WinRT. Не сложилось с C#!

    15 марта 2013 г. 7:50
  • Ну а как мне создать ItemControl и разместив в нем Image, TextBlock и еще что-нить засунуть его в WrapPanel при ItemWidth и ItemHeight в 100 пикселей при горизонтальном выравнивании??? И чтобы потом моно было работать с этим ItemControl (двойной клик, анимашки и т.д. и.т.п) ???

    15 марта 2013 г. 8:12
  • Ну а как мне создать ItemControl и разместив в нем Image, TextBlock и еще что-нить засунуть его в WrapPanel при ItemWidth и ItemHeight в 100 пикселей при горизонтальном выравнивании??? И чтобы потом моно было работать с этим ItemControl (двойной клик, анимашки и т.д. и.т.п) ???

    На VB пример пойдет?

    Женат на WPF. Тайно встречаюсь с WinRT. Не сложилось с C#!

    15 марта 2013 г. 8:57
  • ))) Лучше бы, конечно, на c# все же!

    Ну давай хоть такой.

    Дело в том, что в UserControl я могу визуально построить и настроить все контроллы компонента, а тут как быть?

    15 марта 2013 г. 9:07
  • Дело в том, что в UserControl я могу визуально построить и настроить все контроллы компонента, а тут как быть?

    Пока я не начал делать пример еще раз уточню - в конечно итоге суть только в отображении данных? Если да, то UserControl это лишняя нагрузка. Если же например, вам нужно что бы каждый отдельный элемент коллекции имел функционал, которого нет в обычных элементах, то тут UserControl.

    Объясню подробнее разницу.

    Например у меня есть UserControl под названием UTCControl. Он имеет две кнопки для прибаления и уменьшения значения и поле для отображения значение. Значение может быть от -12 до +12. Это сдвиг часового пояса от значения UTC (то есть 0). Все эти действия не решить разметкой или чем то еще, поэтому требуется UTCControl.

    Пример номер 2. Есть список в котором отображается перечень специальных дат созданных пользователем. Информация о специальной дате имеет тип (например день рождения или новый год), название даты, описание, сама дата и прочее. Все это отображается как элемегты списка причем тип даты показан картинкой, а название, описание и дата отображаются в столбике с разделителем. Иэто все через DataTemplate.

    Итак повторяю вопрос - в конечно итоге суть только в отображении данных?


    Женат на WPF. Тайно встречаюсь с WinRT. Не сложилось с C#!

    15 марта 2013 г. 9:22
  • Нет! Однозначно нужен UserControl. Помимо отображения данных будет реализация настройки элемента списка прям в UserControl (к примеру CheckBox или Slider).

    По поводу моего кода : сволочь еще та!

    Пишу в методе:

    if(<Я тебя вижуууу!!! о_о>)
      Background = new SolidColorBrush(Colors.Red); /// -> РАБОТАЕТ
    
    if(<Я тебя вижуууу!!! о_о>)
      Image.Source = new BitmapImage(new Uri(@"D:\Pictures\Картинки\картинка.jpeg", UriKind.RelativeOrAbsolute)); /// -> НИ ЧЕГО НЕ ПРОИСХОДИТ !!!! ЧТО НЕ ТАК ???

    Вернее все работает только в первый раз. При последующих вызовах метода картинка не грузиться!

    При чем так тоже не работает:

    /// Title это TextBlock
    Action<string> ac = (string text) => { Title.Text = text; };
                Application.Current.Dispatcher.BeginInvoke(ac, "Мой текст");
    
    /// и так тоже
    
    Title.Text = "Мой текст";

    • Изменено Hovanskiy 15 марта 2013 г. 10:01
    15 марта 2013 г. 9:51
  • Нет! Однозначно нужен UserControl. Помимо отображения данных будет реализация настройки элемента списка прям в UserControl (к примеру CheckBox или Slider).

    Вы так и не поняли моих примеров. Добавить CheckBox или Slider в DataTemplate ничего сложного и они будут нормально выполнять свою функцию. При UserControl речь создании того, чего нет в WPF, например линейка.

    В общем я сделаю пример, а вы решите. Сделаю не быстро, сейчас на работе просто.


    Женат на WPF. Тайно встречаюсь с WinRT. Не сложилось с C#!

    15 марта 2013 г. 10:04
  • ОК. Спасибо.

    15 марта 2013 г. 10:20
  • Свою задачу - узнать виден ли компонент - решил:

    private void ImagesListScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
            {
                if ((UnloadedImagesList != null) && (UnloadedImagesList.Count > 0))
                    foreach (ImagesListItem ILI in UnloadedImagesList) ILI.CheckVisible();
            }
    
    
    public async void CheckVisible()
            {
                int itemY = (int)VisualTreeHelper.GetOffset(this).Y;
                int listY = (int)(VisualTreeHelper.GetOffset((this.Parent as WrapPanel)).Y * (-1));
                int scrollH = (int)((this.Parent as WrapPanel).Parent as ScrollViewer).ActualHeight;
                bool show_in_down = ((itemY - listY) < scrollH);
                bool show_in_up = ((itemY + this.ActualHeight) > listY);
                if (show_in_up && show_in_down)
                {
                    ImageSource imgsource = await System.Threading.Tasks.Task.Run(() => { return new System.Drawing.Bitmap(ItemData.FileName).InNewSize(new ImageSize(90, 90), System.Drawing.Drawing2D.InterpolationMode.Low).ToBitmapImage(); });
                    GC.Collect();
                    Image.Source = imgsource;
                    UnloadedImagesList.Remove(this);
                }
            }

    Метод все же считаю костарным - но работает )))

    При загрузке в список более 2000 изображений с фотика (~3000*4000 точек) объем памяти не увеличивается выше 240 Мб, а по окончании загрузки падает до ~20 Мб.

    Использовал я UserControl. Интересно, конечно, глянуть на реализацию такого списка без него.

    • Помечено в качестве ответа Hovanskiy 15 марта 2013 г. 18:06
    15 марта 2013 г. 11:29
  • То есть при каждой прокрутке вы проходите по всему списку из 2000 элементов и проверяете нужно показывать Image или нет? И что это работает достаточно быстро? Мне просто интересно ибо я так делать не пробовал.

    Женат на WPF. Тайно встречаюсь с WinRT. Не сложилось с C#!

    15 марта 2013 г. 11:40
  • Нет нет.

    При загрузке изображений в список, создается список (типа List), в который добавляются эти же элементы.
    При создании элемента ему указывается этот список и в методе CheckVisible, после загрузки изображения этот элемент удаляется из этого списка.

    В итоге, при прокрутке проверяются только те элементы, которые есть в этом списке (которые еще не загружали изображений).

    Да, я говорил - метод кустарный - по ничего другого в голову не пришло.

    15 марта 2013 г. 12:21
  • Вот обещанный проект пример. Проект простенький, без лишнего. Что он делает - сразу при запуске просит выбрать папку с фото. Выбирать можно корневую, подгрузит и из подпапок. Все найденные фотки добавляются в список, а он с помощью встроенных асинхронных возможностей декодирует фото и отображает. Для демонстрации того, что в DataTemplate можно помещать и элементы управления под каждой фоткой есть Slider, который отвечает за прозрачность текущей фото. То есть у любой фотки можно изменить прозрачность.

    Один нюанс. Та как в качестве панели используется WrapPanel не происходит виртуализация и все фото что есть на моем компе отобразившись в окошке не вешали не прогу не комп, но в диспетчере она весила 600 мб. Правда комп у меня мощный. Если же использовать стандартную панель, то есть установку WrapPanel убрать, то получаем загрузку только тех фоток что видим в данный момент и даже когда загрузятся все прога в оперативе 34 мб.

    Итак вы можете почерпнуть важные для WPF моменты и решать как в итоге реализовать вашу задачу.


    Женат на WPF. Тайно встречаюсь с WinRT. Не сложилось с C#!

    15 марта 2013 г. 16:01
  • Спасибо. Изучу ваш код.

    15 марта 2013 г. 16:35
  • Ни как не могу заставить работать ваш код. Перевожу его в C#, вроде все норм. Но постоянно ругается:

    "Ошибка 1 ***(любой класс)*** не реализует член интерфейса "System.ComponentModel.INotifyPropertyChanged.PropertyChanged". "***(любой класс)***.PropertyChanged" не удается реализовать "System.ComponentModel.INotifyPropertyChanged.PropertyChanged", поскольку он не содержит соответствующего типа возвращаемого значения "System.ComponentModel.PropertyChangedEventHandler"...."

    16 марта 2013 г. 8:59
  • О том как классы должны реализовывать INotifyPropertyChanged читайте здесь. Там же есть пример и на C#.

    Женат на WPF. Тайно встречаюсь с WinRT. Не сложилось с C#!

    16 марта 2013 г. 9:01
  • Ну в принципе заставил работать без этого класса.

    Единственное, интересно все же как реализовать отрисовку только видимых итемов при использовании WrapPanel. Или есть дакие другие способы вывести итемы плитками!?

    P.S. И как в BindingList можно проверить элемент на существование?

    (если Элемент.Путь != НовыйЭлемент.Путь => добавляем)

    Пытался сделать так, но не работает:

    if (imageItems.Items.Where(x => x.Path == findFile).Count() == 0)
    {
        imageItems.Items.Add(new ImageItem() { Path = findFile, Title = new FileInfo(findFile).Name });
        GC.Collect();
    }


    • Изменено Hovanskiy 16 марта 2013 г. 10:34
    16 марта 2013 г. 9:31
  • Проблема решена.

    Заменил BindingList на свой класс на его основе с переопределением метода Add:

    private async Task SearchImagesFiles(System.Windows.Forms.FolderBrowserDialog FBD)
            {
                await Task.Run(() =>
                {
                    Action<string> addInListAction = delegate(string findFile)
                    {
                        imageItems.Items.Add(new ImageItem() { Path = findFile, Title = new FileInfo(findFile).Name });
                        GC.Collect();
                    };
                    return SearchFilesInDirectory(FBD.SelectedPath, new List<string>() { "*.jpeg", "*.jpg", "*.png", "*.gif", "*.bmp" }, addInListAction);
                });
            }
    
    public class ImageItems
        {
            public ImageItems()
            {
                Items = new ImageItemList();
            }
            public void OnPropertyChenged(string PropertyName)
            {
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(PropertyName));
                }
            }
            public event PropertyChangedEventHandler PropertyChanged;
            public delegate void PropertyChangedEventHandler(object sender, System.ComponentModel.PropertyChangedEventArgs e);
            private ImageItemList items;
            public ImageItemList Items
            {
                get { return items; }
                set
                {
                    items = value;
                    OnPropertyChenged("Items");
                }
            }
        }
        public class ImageItemList : BindingList<ImageItem>
        {
            public new void Add(ImageItem item)
            {
                if (this.Items.Count(x => x.Path == item.Path) == 0)
                    base.Add(item);
            }
        }

    • Помечено в качестве ответа Hovanskiy 16 марта 2013 г. 11:07
    16 марта 2013 г. 11:07
  • LXGDARK, подскажите, пожалуйста.

    Связая ListBox с DataContext по вашему примеру (VB).

    Как мне удалить больше одного элемента из ListBox???

    Делаю так:

     
    foreach (ImageItem ii in ListBox.SelectedItems)
        ((ImageItemList)ListBox.ItemsSource).Remove(ii);

    Получаю ошибку, что коллекция была изменена и т.д.

    Как быть?

    16 марта 2013 г. 19:40
  • Блин. Час себе голову ломал, не мог сделать! Написал здесь и за  минут решил!?:):):)

    Решение удаления множества элементов из ListBox:

    List<ImageItem> remList = new List<ImageItem>();
    foreach (ImageItem ii in ListBox.SelectedItems)
        remList.Add(ii);
    foreach (ImageItem item in remList)
        ((ImageItemList)ListBox.ItemsSource).Remove(item);
    remList = null;

    • Помечено в качестве ответа Hovanskiy 16 марта 2013 г. 19:52
    16 марта 2013 г. 19:52
  • Олег пожалуйста не засоряйте тему. Вопросы не касающиеся первоначального лучше задавать в отдельных темах.


    Женат на WPF. Тайно встречаюсь с WinRT. Не сложилось с C#!

    • Изменено LXGDARK 25 марта 2013 г. 14:57
    17 марта 2013 г. 6:07