이전에는 목록 상자에 항목을 수동으로 추가하기 위해 뒤에있는 코드를 사용했지만 매우 느 렸습니다. 성능 측면에서 XAML을 통한 데이터 바인딩이 가장 좋은 방법이라고 들었습니다.
그래서 데이터 바인딩이 작동하도록 할 수 있었지만 (바인딩에 처음 사용) 실망스럽게도 이전의 비 데이터 바인딩 방법보다 성능이 더 좋지 않습니다.
아이디어는 내 ListBox에 그 아래에 이름이있는 이미지가 포함되어 있다는 것입니다. 몇 가지 벤치마킹을했고 54 개 항목을 표시하는 데 8 초가 걸립니다. 사용자가 기다리기에는 당연히 너무 깁니다.
소스 이미지는 최대 2100x1535px이고 범위는 파일 당 400kb> 4mb입니다.
이 문제를 재현하는 데 필요한 이미지는 여기에서 찾을 수 있습니다. 질문에 대한 답변으로 제거 된 링크이며 내 서버에 대역폭 허용량이 많지 않습니다. 다른 이미지 소스 : https://imgur.com/a/jmbv6
아래에서 재현 가능한 문제의 예를 만들었습니다. 이걸 너무 느리게 만드는 내가 뭘 잘못하고 있니?
감사합니다.
XAML :
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="600" Width="800" WindowState="Maximized">
<Grid>
<ListBox x:Name="listBoxItems" ItemsSource="{Binding ItemsCollection}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<VirtualizingStackPanel>
<Image Width="278" Height="178">
<Image.Source>
<BitmapImage DecodePixelWidth="278" UriSource="{Binding ImagePath}" CreateOptions="IgnoreColorProfile" />
</Image.Source>
</Image>
<TextBlock Text="{Binding Name}" FontSize="16" VerticalAlignment="Bottom" HorizontalAlignment="Center" />
</VirtualizingStackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
뒤에있는 코드 :
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Threading;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
internal class Item : INotifyPropertyChanged
{
public Item(string name = null)
{
this.Name = name;
}
public string Name { get; set; }
public string ImagePath { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
ObservableCollection<Item> ItemsCollection;
List<Item> data;
public MainWindow()
{
InitializeComponent();
this.data = new List<Item>();
this.ItemsCollection = new ObservableCollection<Item>();
this.listBoxItems.ItemsSource = this.ItemsCollection;
for (int i = 0; i < 49; i ++)
{
Item newItem = new Item
{
ImagePath = String.Format(@"Images/{0}.jpg", i + 1),
Name = "Item: " + i
};
this.data.Add(newItem);
}
foreach (var item in this.data.Select((value, i) => new { i, value }))
{
Dispatcher.Invoke(new Action(() =>
{
this.ItemsCollection.Add(item.value);
}), DispatcherPriority.Background);
}
}
}
}
이제 사용중인 이미지를 볼 수 있으므로 여기서 주요 문제는 단순히 큰 이미지를로드하는 데 드는 기본 비용이라는 것을 확인할 수 있습니다. 그 이미지 파일을 사용하여 그 시간을 개선 할 방법은 없습니다.
할 수있는 일은 이미지를 비동기식으로로드하여 사용자가 모든 이미지가로드 될 때까지 대기하는 동안 프로그램의 나머지 부분이 반응하도록하거나 이미지 크기를 줄여서 더 빨리로드되도록하는 것입니다. 가능하다면 후자를 강력히 추천합니다.
어떤 이유로 이미지를 배포하고 원래의 큰 크기 형식으로로드해야하는 경우 최소한 비동기 적으로로드해야합니다. 이를 수행하는 방법에는 여러 가지가 있습니다.
가장 간단한 방법은 바인딩 을 설정 Binding.IsAsync
하는 것입니다 Image.Source
.
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Image Width="278" Height="178" Source="{Binding ImagePath, IsAsync=True}"/>
<TextBlock Text="{Binding Name}" FontSize="16"
VerticalAlignment="Bottom" HorizontalAlignment="Center" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
이 방법의 가장 큰 단점은이 방법을 사용할 DecoderPixelWidth
때 설정할 수 없다는 것입니다 . Image
제어는 당신을위한 실제 비트 맵의 경로에서 변환을 처리하고, 다양한 옵션을 설정할 수있는 메커니즘이 없습니다.
기술의 단순함을 감안할 때 적어도 나에게는 이것이 선호되는 방법이라고 생각합니다. 사용자는 프로그램이 반응하고 진행 징후를 보이는 한 모든 데이터를 완전히 초기화하는 데 걸리는 총 시간에 신경 쓰지 않는 경우가 많습니다. 그러나이 DecoderPixelWidth
시나리오에서 설정하지 않으면 모든 이미지를로드하는 데 거의 두 배의 시간이 걸렸습니다 (약 7.5 초 대 거의 14 초). 따라서 이미지를 직접 비동기식으로로드하는 데 관심이있을 수 있습니다.
이렇게하려면 이미 익숙한 일반적인 비동기 프로그래밍 기술이 필요합니다. 주된 "문제"는 기본적으로 WPF 비트 맵 처리 클래스가 실제로 필요할 때까지 비트 맵의 실제로드를 연기한다는 것입니다. 데이터를 즉시로드 할 수있는 경우가 아니면 비트 맵을 비동기 적으로 생성하는 것은 도움이되지 않습니다.
다행히도 가능합니다. 단순히 CacheOption
속성을 BitmapCacheOption.OnLoad
.
원본 예제를 정리하고 적절한 뷰 모델 데이터 구조를 만들고 이미지의 비동기로드를 구현할 자유를 얻었습니다. 이런 식으로로드 시간이 8 초 미만이지만로드가 진행되는 동안 UI는 계속 응답합니다. 저는 두 개의 타이머를 포함 시켰습니다. 하나는 프로그램이 시작된 이후의 경과 시간을 표시하고 주로 UI의 응답 성을 설명하기 위해 있고 다른 하나는 비트 맵 이미지를 실제로로드하는 데 소요 된 시간을 보여줍니다.
XAML :
<Window x:Class="TestSO42639506PopulateListBoxImages.MainWindow"
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:l="clr-namespace:TestSO42639506PopulateListBoxImages"
mc:Ignorable="d"
WindowState="Maximized"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel>
<TextBlock Text="{Binding TotalSeconds, StringFormat=Total seconds: {0:0}}"/>
<TextBlock Text="{Binding LoadSeconds, StringFormat=Load seconds: {0:0.000}}"/>
</StackPanel>
<ListBox x:Name="listBoxItems" ItemsSource="{Binding Data}"
Grid.Row="1"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Image Width="278" Height="178" Source="{Binding Bitmap}"/>
<TextBlock Text="{Binding Name}" FontSize="16"
VerticalAlignment="Bottom" HorizontalAlignment="Center" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
씨#:
class NotifyPropertyChangedBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void _UpdatePropertyField<T>(
ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
{
return;
}
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
class Item : NotifyPropertyChangedBase
{
private string _name;
private string _imagePath;
private BitmapSource _bitmap;
public string Name
{
get { return _name; }
set { _UpdatePropertyField(ref _name, value); }
}
public string ImagePath
{
get { return _imagePath; }
set { _UpdatePropertyField(ref _imagePath, value); }
}
public BitmapSource Bitmap
{
get { return _bitmap; }
set { _UpdatePropertyField(ref _bitmap, value); }
}
}
class MainWindowModel : NotifyPropertyChangedBase
{
public MainWindowModel()
{
_RunTimer();
}
private async void _RunTimer()
{
Stopwatch sw = Stopwatch.StartNew();
while (true)
{
await Task.Delay(1000);
TotalSeconds = sw.Elapsed.TotalSeconds;
}
}
private ObservableCollection<Item> _data = new ObservableCollection<Item>();
public ObservableCollection<Item> Data
{
get { return _data; }
}
private double _totalSeconds;
public double TotalSeconds
{
get { return _totalSeconds; }
set { _UpdatePropertyField(ref _totalSeconds, value); }
}
private double _loadSeconds;
public double LoadSeconds
{
get { return _loadSeconds; }
set { _UpdatePropertyField(ref _loadSeconds, value); }
}
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
partial class MainWindow : Window
{
private readonly MainWindowModel _model = new MainWindowModel();
public MainWindow()
{
DataContext = _model;
InitializeComponent();
_LoadItems();
}
private async void _LoadItems()
{
foreach (Item item in _GetItems())
{
_model.Data.Add(item);
}
foreach (Item item in _model.Data)
{
BitmapSource itemBitmap = await Task.Run(() =>
{
Stopwatch sw = Stopwatch.StartNew();
BitmapImage bitmap = new BitmapImage();
bitmap.BeginInit();
// forces immediate load on EndInit() call
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.UriSource = new Uri(item.ImagePath, UriKind.Relative);
bitmap.DecodePixelWidth = 278;
bitmap.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
bitmap.EndInit();
bitmap.Freeze();
sw.Stop();
_model.LoadSeconds += sw.Elapsed.TotalSeconds;
return bitmap;
});
item.Bitmap = itemBitmap;
}
}
private static IEnumerable<Item> _GetItems()
{
for (int i = 1; i <= 60; i++)
{
Item newItem = new Item
{
ImagePath = String.Format(@"Images/{0}.jpg", i),
Name = "Item: " + i
};
yield return newItem;
}
}
}
방금 .zip에서 내 프로젝트 디렉토리로 파일을 직접 복사했기 때문에 원래 예제와 같이 1-49 대신 1-60과 같은 실제 파일 이름에 해당하도록 이미지 경로 루프를 변경했습니다. 나는 또한 0 기반 레이블을 신경 쓰지 않고 대신 파일 이름과 동일하게 만들었습니다.
여기에 당신의 질문을 직접적으로 다루는 또 다른 질문이 있는지 알아보기 위해 조금 둘러 보았습니다. 정확히 중복 된 것으로 생각되는 것을 찾지 못했지만 WPF를 사용하여 C #에서 BitmapImage를 비동기식으로로드하는 매우 광범위한 기술이 있는데, 위와 유사하거나 동일한 기술을 포함하여 여러 기술을 보여줍니다.
이 기사는 인터넷에서 수집됩니다. 재 인쇄 할 때 출처를 알려주십시오.
침해가 발생한 경우 연락 주시기 바랍니다[email protected] 삭제
몇 마디 만하겠습니다