Оказалось, что медленная часть - это вызов Image.SelectActiveFrame.
Как обычно, решение caching. Однако, чтобы не увеличивать начальное время загрузки, оно должно выполняться лениво на фоне.
Идея проста. Запустите рабочий поток и загрузите все кадры изображения как отдельные Bitmap
s в массив. Затем используйте кешированное изображение из массива вместо SelectActiveFrame
.
Поскольку все, что требует некоторой синхронизации потоков, я инкапсулированные его в вспомогательном классе:
class PageBuffer : IDisposable
{
public static PageBuffer Open(string path)
{
return new PageBuffer(File.OpenRead(path));
}
private PageBuffer(Stream stream)
{
this.stream = stream;
Source = Image.FromStream(stream);
PageCount = Source.GetFrameCount(FrameDimension.Page);
if (PageCount < 2) return;
pages = new Image[PageCount];
var worker = new Thread(LoadPages) { IsBackground = true };
worker.Start();
}
private void LoadPages()
{
for (int index = 0; ; index++)
{
lock (syncLock)
{
if (disposed) return;
if (index >= pages.Length)
{
// If you don't need the source image,
// uncomment the following line to free some resources
//DisposeSource();
return;
}
if (pages[index] == null)
pages[index] = LoadPage(index);
}
}
}
private Image LoadPage(int index)
{
Source.SelectActiveFrame(FrameDimension.Page, index);
return new Bitmap(Source);
}
private Stream stream;
private Image[] pages;
private object syncLock = new object();
private bool disposed;
public Image Source { get; private set; }
public int PageCount { get; private set; }
public Image GetPage(int index)
{
if (disposed) throw new ObjectDisposedException(GetType().Name);
if (PageCount < 2) return Source;
var image = pages[index];
if (image == null)
{
lock (syncLock)
{
image = pages[index];
if (image == null)
image = pages[index] = LoadPage(index);
}
}
return image;
}
public void Dispose()
{
if (disposed) return;
lock (syncLock)
{
disposed = true;
if (pages != null)
{
foreach (var item in pages)
if (item != null) item.Dispose();
pages = null;
}
DisposeSource();
}
}
private void DisposeSource()
{
if (Source != null)
{
Source.Dispose();
Source = null;
}
if (stream != null)
{
stream.Dispose();
stream = null;
}
}
}
Полный рабочий демо:
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Threading;
using System.Windows.Forms;
namespace Demo
{
class TestForm : Form
{
public TestForm()
{
var panel = new Panel { Dock = DockStyle.Top, BorderStyle = BorderStyle.FixedSingle };
openButton = new Button { Text = "Open", Top = 8, Left = 16 };
prevButton = new Button { Text = "Prev", Top = 8, Left = 16 + openButton.Right };
nextButton = new Button { Text = "Next", Top = 8, Left = 16 + prevButton.Right };
panel.Height = 16 + openButton.Height;
panel.Controls.AddRange(new Control[] { openButton, prevButton, nextButton });
pageViewer = new PictureBox { Dock = DockStyle.Fill, SizeMode = PictureBoxSizeMode.Zoom };
ClientSize = new Size(850, 1100 + panel.Height);
Controls.AddRange(new Control[] { panel, pageViewer });
openButton.Click += OnOpenButtonClick;
prevButton.Click += OnPrevButtonClick;
nextButton.Click += OnNextButtonClick;
Disposed += OnFormDisposed;
UpdatePageInfo();
}
private Button openButton;
private Button prevButton;
private Button nextButton;
private PictureBox pageViewer;
private PageBuffer pageData;
private int currentPage;
private void OnOpenButtonClick(object sender, EventArgs e)
{
using (var dialog = new OpenFileDialog())
{
if (dialog.ShowDialog(this) == DialogResult.OK)
Open(dialog.FileName);
}
}
private void OnPrevButtonClick(object sender, EventArgs e)
{
SelectPage(currentPage - 1);
}
private void OnNextButtonClick(object sender, EventArgs e)
{
SelectPage(currentPage + 1);
}
private void OnFormDisposed(object sender, EventArgs e)
{
if (pageData != null)
pageData.Dispose();
}
private void Open(string path)
{
var data = PageBuffer.Open(path);
pageViewer.Image = null;
if (pageData != null)
pageData.Dispose();
pageData = data;
SelectPage(0);
}
private void SelectPage(int index)
{
pageViewer.Image = pageData.GetPage(index);
currentPage = index;
UpdatePageInfo();
}
private void UpdatePageInfo()
{
prevButton.Enabled = pageData != null && currentPage > 0;
nextButton.Enabled = pageData != null && currentPage < pageData.PageCount - 1;
}
}
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new TestForm());
}
}
class PageBuffer : IDisposable
{
public static PageBuffer Open(string path)
{
return new PageBuffer(File.OpenRead(path));
}
private PageBuffer(Stream stream)
{
this.stream = stream;
Source = Image.FromStream(stream);
PageCount = Source.GetFrameCount(FrameDimension.Page);
if (PageCount < 2) return;
pages = new Image[PageCount];
var worker = new Thread(LoadPages) { IsBackground = true };
worker.Start();
}
private void LoadPages()
{
for (int index = 0; ; index++)
{
lock (syncLock)
{
if (disposed) return;
if (index >= pages.Length)
{
// If you don't need the source image,
// uncomment the following line to free some resources
//DisposeSource();
return;
}
if (pages[index] == null)
pages[index] = LoadPage(index);
}
}
}
private Image LoadPage(int index)
{
Source.SelectActiveFrame(FrameDimension.Page, index);
return new Bitmap(Source);
}
private Stream stream;
private Image[] pages;
private object syncLock = new object();
private bool disposed;
public Image Source { get; private set; }
public int PageCount { get; private set; }
public Image GetPage(int index)
{
if (disposed) throw new ObjectDisposedException(GetType().Name);
if (PageCount < 2) return Source;
var image = pages[index];
if (image == null)
{
lock (syncLock)
{
image = pages[index];
if (image == null)
image = pages[index] = LoadPage(index);
}
}
return image;
}
public void Dispose()
{
if (disposed) return;
lock (syncLock)
{
disposed = true;
if (pages != null)
{
foreach (var item in pages)
if (item != null) item.Dispose();
pages = null;
}
DisposeSource();
}
}
private void DisposeSource()
{
if (Source != null)
{
Source.Dispose();
Source = null;
}
if (stream != null)
{
stream.Dispose();
stream = null;
}
}
}
}
UPDATE: Как уже упоминалось в комментариях , приведенная выше реализация использует довольно простую жадную стратегию кэширования, которая использует много памяти и не работает для больших файлов.
Хорошо, однако, что после того, как логика инкапсулируется внутри класса, мы можем изменить стратегию, не касаясь нашего кода приложения.Например, мы можем удалить кэширование на всех (возвращение в исходное состояние), или оптимизировать «prev/next» навигация поддерживая небольшой набор кэшированных «окно» изображения, как этот
class PageBuffer : IDisposable
{
public const int DefaultCacheSize = 5;
public static PageBuffer Open(string path, int cacheSize = DefaultCacheSize)
{
return new PageBuffer(File.OpenRead(path), cacheSize);
}
private PageBuffer(Stream stream, int cacheSize)
{
this.stream = stream;
source = Image.FromStream(stream);
pageCount = source.GetFrameCount(FrameDimension.Page);
if (pageCount < 2) return;
pageCache = new Image[Math.Min(pageCount, Math.Max(cacheSize, 3))];
var worker = new Thread(LoadPages) { IsBackground = true };
worker.Start();
}
private void LoadPages()
{
while (true)
{
lock (syncLock)
{
if (disposed) return;
int index = Array.FindIndex(pageCache, 0, pageCacheSize, p => p == null);
if (index < 0)
Monitor.Wait(syncLock);
else
pageCache[index] = LoadPage(pageCacheStart + index);
}
}
}
private Image LoadPage(int index)
{
source.SelectActiveFrame(FrameDimension.Page, index);
return new Bitmap(source);
}
private Stream stream;
private Image source;
private int pageCount;
private Image[] pageCache;
private int pageCacheStart, pageCacheSize;
private object syncLock = new object();
private bool disposed;
public Image Source { get { return source; } }
public int PageCount { get { return pageCount; } }
public Image GetPage(int index)
{
if (disposed) throw new ObjectDisposedException(GetType().Name);
if (PageCount < 2) return Source;
lock (syncLock)
{
AdjustPageCache(index);
int cacheIndex = index - pageCacheStart;
var image = pageCache[cacheIndex];
if (image == null)
image = pageCache[cacheIndex] = LoadPage(index);
return image;
}
}
private void AdjustPageCache(int pageIndex)
{
int start, end;
if ((start = pageIndex - pageCache.Length/2) <= 0)
end = (start = 0) + pageCache.Length;
else if ((end = start + pageCache.Length) >= PageCount)
start = (end = PageCount) - pageCache.Length;
if (start < pageCacheStart)
{
int shift = pageCacheStart - start;
if (shift >= pageCacheSize)
ClearPageCache(0, pageCacheSize);
else
{
ClearPageCache(pageCacheSize - shift, pageCacheSize);
for (int j = pageCacheSize - 1, i = j - shift; i >= 0; j--, i--)
Exchange(ref pageCache[i], ref pageCache[j]);
}
}
else if (start > pageCacheStart)
{
int shift = start - pageCacheStart;
if (shift >= pageCacheSize)
ClearPageCache(0, pageCacheSize);
else
{
ClearPageCache(0, shift);
for (int j = 0, i = shift; i < pageCacheSize; j++, i++)
Exchange(ref pageCache[i], ref pageCache[j]);
}
}
if (pageCacheStart != start || pageCacheStart + pageCacheSize != end)
{
pageCacheStart = start;
pageCacheSize = end - start;
Monitor.Pulse(syncLock);
}
}
void ClearPageCache(int start, int end)
{
for (int i = start; i < end; i++)
Dispose(ref pageCache[i]);
}
static void Dispose<T>(ref T target) where T : class, IDisposable
{
var value = target;
if (value != null) value.Dispose();
target = null;
}
static void Exchange<T>(ref T a, ref T b) { var c = a; a = b; b = c; }
public void Dispose()
{
if (disposed) return;
lock (syncLock)
{
disposed = true;
if (pageCache != null)
{
ClearPageCache(0, pageCacheSize);
pageCache = null;
}
Dispose(ref source);
Dispose(ref stream);
if (pageCount > 2)
Monitor.Pulse(syncLock);
}
}
}
или осуществлять другие «умная» стратегия кэширования. Мы можем даже выбрать стратегию, выбрав Strategy pattern.
Бу, который будет другой историей. Вторая реализация PageBuffer
должна быть достаточной для случая использования ОП.
Почему бы вам не поставить точки останова в коде, особенно в коде, где вы переходите на следующую страницу .. и посмотреть, можете ли вы определить точку бутылки. Я ненавижу использование 'Application.DoEvents()' однако после вы вызываете метод .Refresh, попробуйте положить вызов 'Application.DoEvents() после этого – MethodMan
Я использовал точки останова. Любопытно, почему вы ненавидите использование 'Application.DoEvents()'? @MethodMan – taji01
Читайте на «Про» и «Недостатки» Application.DoEvents() 'есть тонны сообщений на' SO' об этом, а также другие, которые расскажут вам об этом, но я просто делал личное мнение/утверждение о том, почему я ненавижу использовать его .. это не стоит вдаваться в этом отношении ... – MethodMan