Introduction
ImageFan is a lightweight image viewer for .NET 2.0, supporting multi-core processing. At the moment, it is in alpha stage in terms of the features exposed, but stable on the existing functionality.
Background
I always wanted to exploit the capabilities of .NET to create an image viewer offering a managed (possibly portable due to Mono) counterpart to the existing C / C++ solutions. Also, the lack of 64 bit image viewers enticed me to pursue a .NET solution that would implicitly run as a 64 bit application, when the hardware and software platform would allow it.
Visual Layout and Functionality
The application is designed in the traditional style of contemporary image viewers as follows:
- A drives list and corresponding directories tree on the left side of the window
- A thumbnails list on the right side, revealing thumbnails to the images contained inside the selected drive and directory
Selecting a drive refreshes the list of directories and reveals the thumbnails to the images contained on the root drive. Selecting a specific directory fills the thumbnails list with the appropriate thumbnails. The thumbnails list can be navigated on by using the arrow keys and the scroll bar.
When the left mouse button is clicked or the Enter key pressed while on a thumbnail a new window is opened, containing the image in full size, scrollable if at least one dimension of the image is greater than the corresponding screen size dimension. The user can traverse the images list using this window, by employing the Space and Backspace keys and the arrow keys. The window can be closed by pressing the Esc key or the closing button on the window.
If the image window is clicked on again or pressed Enter upon, it will set the image to full screen view, resizing it if necessary to fit on the screen. In the full screen mode, the thumbnails list is navigable with the Space and Backspace keys, the arrow keys and the mouse wheel. The user can exit this mode by pressing the Esc key or the left mouse button.
Implementation Challenges and Constraints
The project structure is revealed in the diagram below. I will explain each source code artifact in turn.
Class TasksDispatcher
This class is a general tasks dispatcher that partitions an array of tasks to the number of available processor cores, where each task depends solely on an object parameter.
For the problem at hand, I used the tasks dispatcher class to partition (by their indexes) the collection of image thumbnails, which is to be displayed asynchronously when the user selects a directory or drive.
namespace ImageFan
{
class TasksDispatcher
{
private const int MaxThreadJoinTime = 250;
public delegate void ProcessingTask(object param);
private static readonly int ProcessorCount;
private static readonly Thread[] Threads;
private ProcessingTask globalTask;
private ProcessingTask individualTask;
private int tasksCount;
private volatile bool dispatcherIsActive;
private Thread workerThread;
static TasksDispatcher()
{
ProcessorCount = Environment.ProcessorCount;
Threads = new Thread[ProcessorCount];
}
public TasksDispatcher(ProcessingTask globalTask, ProcessingTask individualTask, int tasksCount)
{
this.globalTask = globalTask;
this.individualTask = individualTask;
this.tasksCount = tasksCount;
}
public void Start()
{
workerThread = new Thread(new ThreadStart(WorkerThreadLoopMethod));
workerThread.Start();
}
public void Stop()
{
if (dispatcherIsActive == true)
{
dispatcherIsActive = false;
if ((workerThread != null) && (workerThread.ThreadState != ThreadState.Unstarted))
{
if (!workerThread.Join(MaxThreadJoinTime))
{
foreach (Thread aThread in Threads)
if ((aThread != null) && (aThread.ThreadState != ThreadState.Unstarted))
{
aThread.Abort();
aThread.Join();
}
workerThread.Abort();
workerThread.Join();
}
}
}
}
private void IndividualTaskAbortSafe(object param)
{
try
{
individualTask(param);
}
catch (ThreadAbortException)
{
Thread.ResetAbort();
}
}
private void WorkerThreadLoopMethod()
{
try
{
dispatcherIsActive = true;
for (int i = 0; (i < tasksCount) && (dispatcherIsActive); i += ProcessorCount)
{
int j;
for (j = 0; (j < ProcessorCount) && (i + j < tasksCount) && (dispatcherIsActive); j++)
globalTask(i + j);
for (j = 0; (j < ProcessorCount) && (i + j < tasksCount) && (dispatcherIsActive); j++)
{
Threads[j] = new Thread(new ParameterizedThreadStart(IndividualTaskAbortSafe));
Threads[j].Start(i + j);
}
for (j = 0; (j < ProcessorCount) && (i + j < tasksCount); j++)
if ((Threads[j] != null) && (Threads[j].ThreadState != ThreadState.Unstarted))
Threads[j].Join();
}
dispatcherIsActive = false;
}
catch (ThreadAbortException)
{
Thread.ResetAbort();
}
}
}
}
Class FolderTreeView
This class is a Windows Form custom control, extending the TreeView control. It displays the folders (directories) on the selected drive in a tree-like manner and, when a directory node is selected, triggers the display of thumbnails in the ThumbnailsSequence control.
namespace ImageFan
{
public class FolderTreeView : TreeView
{
private TreeNode currentNode;
public FolderTreeView()
: base()
{
this.HideSelection = false;
this.PathSeparator = Path.DirectorySeparatorChar.ToString();
this.AfterCollapse += new TreeViewEventHandler(FolderTreeView_AfterCollapse);
this.AfterExpand += new TreeViewEventHandler(FolderTreeView_AfterExpand);
this.MouseMove += new MouseEventHandler(FolderTreeView_MouseEnter);
}
public string Folder
{
get
{
if (this.SelectedNode != null)
currentNode = this.SelectedNode;
if (currentNode == null)
return null;
else
return (string)currentNode.Tag;
}
set
{
if (value != null)
{
this.Nodes.Clear();
currentNode = null;
bool folderIsAccessible = true;
try
{
DirectoryInfo dirInfo = new DirectoryInfo(value);
dirInfo.GetDirectories();
}
catch
{
folderIsAccessible = false;
}
if (folderIsAccessible == true)
{
currentNode = new TreeNode(value);
currentNode.Tag = value;
this.Nodes.Add(currentNode);
BuildSubfoldersList(currentNode, value);
currentNode.Expand();
}
}
}
}
private void FolderTreeView_AfterExpand(object sender, TreeViewEventArgs e)
{
currentNode = e.Node;
currentNode.Nodes.Clear();
BuildSubfoldersList(currentNode, (string)e.Node.Tag);
}
private void FolderTreeView_AfterCollapse(object sender, TreeViewEventArgs e)
{
currentNode = e.Node;
}
private void FolderTreeView_MouseEnter(object sender, MouseEventArgs e)
{
if (!this.Focused)
this.Focus();
}
private void BuildSubfoldersList(TreeNode node, string newFolder)
{
try
{
DirectoryInfo dirInfo = new DirectoryInfo(newFolder);
foreach (DirectoryInfo di in dirInfo.GetDirectories())
{
try
{
TreeNode newFolderNode = new TreeNode(di.Name);
newFolderNode.Tag = (string)di.FullName;
node.Nodes.Add(newFolderNode);
if (di.GetDirectories().Length > 0)
newFolderNode.Nodes.Add(new TreeNode());
}
catch { }
}
}
catch { }
}
}
}
Class ThumbnailBox
This type is a Windows Form custom control, inheriting from the UserControl class. It is a fixed-size control (for uniformity) that displays an image thumbnail box, containing the image thumbnail itself, decorated with the image file name.

namespace ImageFan
{
public partial class ThumbnailBox : UserControl
{
private const int ImageNameHeight = 30;
private const int SeparatorHeight = 5;
private ThumbnailsSequence thumbnailsSequence;
private int thumbnailIndex;
private ImageFile imageFile;
private delegate void SetThumbnailImageThreadDelegate(Image thumbnail);
public ThumbnailBox(ThumbnailsSequence thumbnailsSequence, int thumbnailIndex, ImageFile imageFile)
{
InitializeComponent();
this.thumbnailsSequence = thumbnailsSequence;
this.thumbnailIndex = thumbnailIndex;
this.imageFile = imageFile;
SetLoadingImageThumbnail();
}
public int ThumbnailIndex
{
get { return thumbnailIndex; }
set { thumbnailIndex = value; }
}
public ImageFile Image
{
get { return imageFile; }
}
public void SetThumbnailImageThread()
{
Image anImage = imageFile.Thumbnail;
if (this.InvokeRequired)
this.Invoke(new SetThumbnailImageThreadDelegate(SetThumbnailImageHelper), anImage);
else
SetThumbnailImageHelper(anImage);
}
public void ReadImageFromDiscThread()
{
imageFile.GetImageFromFile();
}
private void SetThumbnailImageHelper(Image anImage)
{
pbThumbnail.Image = anImage;
}
private void SetLoadingImageThumbnail()
{
pbThumbnail.Image = GlobalData.LoadingImageThumbnail;
pbThumbnail.Size = new Size(GlobalData.ThumbnailSize, GlobalData.ThumbnailSize);
lbImageName.Size = new Size(GlobalData.ThumbnailSize, ImageNameHeight);
lbImageName.Text = imageFile.ShortFileName;
this.Size = new Size(GlobalData.ThumbnailSize, GlobalData.ThumbnailSize + ImageNameHeight + SeparatorHeight);
}
public void SetBorder(BorderStyle borderStyle)
{
this.BorderStyle = borderStyle;
}
protected override bool IsInputKey(Keys keyData)
{
switch (keyData)
{
case Keys.Up:
case Keys.Down:
case Keys.Left:
case Keys.Right:
return true;
default:
return base.IsInputKey(keyData);
}
}
private void ThumbnailBox_Load(object sender, EventArgs e)
{
foreach (Control aControl in this.Controls)
{
aControl.Cursor = this.Cursor;
aControl.MouseClick += new MouseEventHandler(ThumbnailBox_MouseClick);
}
}
private void ThumbnailBox_MouseClick(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
thumbnailsSequence.MoveToThumbnail(this);
new ImageForm(thumbnailsSequence, imageFile).ShowDialog();
}
}
private void ThumbnailBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
{
if (thumbnailsSequence.CurrentThumbnail != null)
new ImageForm(thumbnailsSequence, thumbnailsSequence.CurrentThumbnail.Image).ShowDialog();
}
else if ((e.KeyCode == Keys.Up) || (e.KeyCode == Keys.W))
thumbnailsSequence.MoveToUpperThumbnail();
else if ((e.KeyCode == Keys.Down) || (e.KeyCode == Keys.S))
thumbnailsSequence.MoveToLowerThumbnail();
else if ((e.KeyCode == Keys.Left) || (e.KeyCode == Keys.A))
thumbnailsSequence.MoveToLeftThumbnail();
else if ((e.KeyCode == Keys.Right) || (e.KeyCode == Keys.D))
thumbnailsSequence.MoveToRightThumbnail();
}
}
}
Class ThumbnailsSequence
The class ThumbnailsSequence is derived from a FlowLayout panel and used as a container for the thumbnails generated at the point of selecting the encompassing directory on the disc.
namespace ImageFan
{
public class ThumbnailsSequence : FlowLayoutPanel
{
private delegate ThumbnailBox CreateThumbnailBoxDelegate(int i);
private string currentDirectory;
private List<ImageFile> imageFiles;
private TasksDispatcher tasksDispatcher;
private ThumbnailBox currentThumbnail;
private bool allowSetThumbnailBorder;
private bool pendingAction;
public ThumbnailsSequence()
: base()
{
this.DoubleBuffered = true;
this.allowSetThumbnailBorder = true;
this.pendingAction = false;
this.VerticalScroll.SmallChange = 25;
this.VerticalScroll.LargeChange = 50;
this.imageFiles = new List<ImageFile>();
this.MouseMove += new MouseEventHandler(ThumbnailsSequence_MouseMove);
}
public void SetPendingAction()
{
this.pendingAction = true;
}
private void ThumbnailsSequence_MouseMove(object sender, MouseEventArgs e)
{
if ((currentThumbnail != null) && (!currentThumbnail.Focused))
currentThumbnail.Focus();
}
private void AddNewThumbnails()
{
try
{
ImageFolder container = new ImageFolder(currentDirectory);
imageFiles = container.ImagesList;
}
catch { }
}
public void DisposeOfPreviousThumbnails()
{
this.currentThumbnail = null;
foreach (ThumbnailBox thumbnail in this.Controls)
thumbnail.Dispose();
this.Controls.Clear();
foreach (ImageFile anImageFile in imageFiles)
anImageFile.Dispose();
}
public void ShowThumbnails()
{
this.SuspendLayout();
this.DisposeOfPreviousThumbnails();
this.AutoScrollPosition = new Point(0, 0);
this.AddNewThumbnails();
this.ResumeLayout(true);
this.SetThumbnails();
}
public string CurrentDirectory
{
set
{
this.currentDirectory = value;
}
}
public ThumbnailBox CurrentThumbnail
{
get { return currentThumbnail; }
set { currentThumbnail = value; }
}
private void SetThumbnailBorder(BorderStyle borderStyle)
{
if (allowSetThumbnailBorder == true)
{
if (currentThumbnail != null)
{
currentThumbnail.SetBorder(borderStyle);
currentThumbnail.Select();
}
}
}
public void MoveToThumbnail(ThumbnailBox thumbnail)
{
if (thumbnail != null)
{
SetThumbnailBorder(BorderStyle.None);
currentThumbnail = thumbnail;
SetThumbnailBorder(BorderStyle.Fixed3D);
ScrollControlIntoView(currentThumbnail);
}
}
public void MoveToNextThumbnail()
{
MoveToThumbnail(this.NextThumbnail);
}
public void MoveToPreviousThumbnail()
{
MoveToThumbnail(this.PreviousThumbnail);
}
public void MoveToUpperThumbnail()
{
MoveToThumbnail(this.UpperThumbnail);
}
public void MoveToLowerThumbnail()
{
MoveToThumbnail(this.LowerThumbnail);
}
public void MoveToLeftThumbnail()
{
MoveToThumbnail(this.LeftThumbnail);
}
public void MoveToRightThumbnail()
{
MoveToThumbnail(this.RightThumbnail);
}
public ThumbnailBox NextThumbnail
{
get
{
if (currentThumbnail == null)
{
if (this.Controls.Count > 0)
return (ThumbnailBox)this.Controls[0];
else
return null;
}
else
{
if (currentThumbnail.ThumbnailIndex < this.Controls.Count - 1)
return (ThumbnailBox)this.Controls[currentThumbnail.ThumbnailIndex + 1];
else
return null;
}
}
}
public ThumbnailBox PreviousThumbnail
{
get
{
if (currentThumbnail == null)
{
if (this.Controls.Count > 0)
return (ThumbnailBox)this.Controls[this.Controls.Count - 1];
else
return null;
}
else
{
if (currentThumbnail.ThumbnailIndex > 0)
return (ThumbnailBox)this.Controls[currentThumbnail.ThumbnailIndex - 1];
else
return null;
}
}
}
public ThumbnailBox UpperThumbnail
{
get
{
if (currentThumbnail == null)
{
if (this.Controls.Count > 0)
return (ThumbnailBox)this.Controls[0];
else
return null;
}
else
{
allowSetThumbnailBorder = false;
ThumbnailBox currentThumbnailCopy = currentThumbnail;
ThumbnailBox upperThumbnail = null;
MoveToPreviousThumbnail();
while ((this.PreviousThumbnail != null) &&
(currentThumbnailCopy.Location.X != currentThumbnail.Location.X))
MoveToPreviousThumbnail();
if ((currentThumbnailCopy.Location.X == currentThumbnail.Location.X) &&
(currentThumbnailCopy.Location.Y > currentThumbnail.Location.Y))
upperThumbnail = currentThumbnail;
MoveToThumbnail(currentThumbnailCopy);
allowSetThumbnailBorder = true;
return upperThumbnail;
}
}
}
public ThumbnailBox LowerThumbnail
{
get
{
if (currentThumbnail == null)
{
if (this.Controls.Count > 0)
return (ThumbnailBox)this.Controls[0];
else
return null;
}
else
{
allowSetThumbnailBorder = false;
ThumbnailBox currentThumbnailCopy = currentThumbnail;
ThumbnailBox lowerThumbnail = null;
MoveToNextThumbnail();
while ((this.NextThumbnail != null) &&
(currentThumbnailCopy.Location.X != currentThumbnail.Location.X))
MoveToNextThumbnail();
if ((currentThumbnailCopy.Location.X == currentThumbnail.Location.X) &&
(currentThumbnailCopy.Location.Y < currentThumbnail.Location.Y))
lowerThumbnail = currentThumbnail;
MoveToThumbnail(currentThumbnailCopy);
allowSetThumbnailBorder = true;
return lowerThumbnail;
}
}
}
public ThumbnailBox LeftThumbnail
{
get
{
if (currentThumbnail == null)
{
if (this.Controls.Count > 0)
return (ThumbnailBox)this.Controls[0];
else
return null;
}
else
{
ThumbnailBox leftThumbnail = this.PreviousThumbnail;
if ((leftThumbnail != null) && (leftThumbnail.Location.Y != currentThumbnail.Location.Y))
leftThumbnail = null;
return leftThumbnail;
}
}
}
public ThumbnailBox RightThumbnail
{
get
{
if (currentThumbnail == null)
{
if (this.Controls.Count > 0)
return (ThumbnailBox)this.Controls[0];
else
return null;
}
else
{
ThumbnailBox rightThumbnail = this.NextThumbnail;
if ((rightThumbnail != null) && (rightThumbnail.Location.Y != currentThumbnail.Location.Y))
rightThumbnail = null;
return rightThumbnail;
}
}
}
private void SetThumbnails()
{
tasksDispatcher = new TasksDispatcher(GenerateThumbnail, SetThumbnailImage, imageFiles.Count);
tasksDispatcher.Start();
}
private void GenerateThumbnail(object index)
{
ThumbnailBox tb;
if (this.InvokeRequired)
tb = (ThumbnailBox)this.Invoke(new CreateThumbnailBoxDelegate(GenerateThumbnailHelper), (int)index);
else
tb = GenerateThumbnailHelper((int)index);
tb.ReadImageFromDiscThread();
}
private ThumbnailBox GenerateThumbnailHelper(int i)
{
ThumbnailBox tb = new ThumbnailBox(this, i, imageFiles[i]);
this.Controls.Add(tb);
if (currentThumbnail == null)
MoveToThumbnail(tb);
return tb;
}
private void SetThumbnailImage(object index)
{
ThumbnailBox aThumbnailBox = (ThumbnailBox)this.Controls[(int)index];
aThumbnailBox.SetThumbnailImageThread();
}
public void StopThumbnailsGeneration()
{
if (tasksDispatcher != null)
tasksDispatcher.Stop();
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
if ((pendingAction) && (currentThumbnail != null))
{
ScrollControlIntoView(currentThumbnail);
pendingAction = false;
}
}
}
}
Class ImageForm
ImageForm is a Windows Form that is shown as a dialog when the user clicks on a particular image thumbnail, as the current image when keyboard-navigating on the images inside the directory or after escaping the full screen mode. The form preserves the image size, enabling scrolling in case the image exceeds the screen size.
namespace ImageFan
{
partial class ImageForm : Form
{
private const int FormScreenBorders = 12;
private const int ScrollDistance = 18;
private ThumbnailsSequence thumbnailsSequence;
private ImageFile imageFile;
private Image image;
private int maxWidth, maxHeight;
public ImageForm(ThumbnailsSequence thumbnailsSequence, ImageFile imageFile)
{
InitializeComponent();
this.thumbnailsSequence = thumbnailsSequence;
maxWidth = Screen.PrimaryScreen.Bounds.Size.Width - FormScreenBorders;
maxHeight = Screen.PrimaryScreen.Bounds.Size.Height - FormScreenBorders;
this.MaximumSize = new Size(maxWidth, maxHeight);
ImageSetUp(imageFile);
}
public void ImageSetUp(ImageFile imageFile)
{
if ((pbImage.Image != null) && (pbImage.Image != GlobalData.InvalidImage))
pbImage.Image.Dispose();
if ((image != null) && (image != GlobalData.InvalidImage))
image.Dispose();
imageFile.Dispose();
this.imageFile = imageFile;
this.image = imageFile.FullSizeImage;
this.Text = imageFile.ShortFileName;
this.pbImage.Image = image;
this.pbImage.Size = image.Size;
this.ClientSize = new Size(Math.Min(pbImage.Size.Width, maxWidth), Math.Min(pbImage.Size.Height, maxHeight));
if ((this.HorizontalScroll.Visible) && (this.Width + ScrollDistance <= maxWidth))
this.Width += ScrollDistance;
if ((this.VerticalScroll.Visible) && (this.Height + ScrollDistance <= maxHeight))
this.Height += ScrollDistance;
this.CenterToScreen();
}
private void ImageForm_FormClosed(object sender, FormClosedEventArgs e)
{
this.Dispose();
thumbnailsSequence.SetPendingAction();
MainForm.Instance.Activate();
}
private void ImageForm_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
new FullScreenImage(this, thumbnailsSequence, imageFile).ShowDialog();
else if ((e.KeyCode == Keys.Space) || (e.KeyCode == Keys.Down) || (e.KeyCode == Keys.Right))
{
if (thumbnailsSequence.NextThumbnail != null)
{
thumbnailsSequence.MoveToNextThumbnail();
ImageSetUp(thumbnailsSequence.CurrentThumbnail.Image);
}
}
else if ((e.KeyCode == Keys.Back) || (e.KeyCode == Keys.Up) || (e.KeyCode == Keys.Left))
{
if (thumbnailsSequence.PreviousThumbnail != null)
{
thumbnailsSequence.MoveToPreviousThumbnail();
ImageSetUp(thumbnailsSequence.CurrentThumbnail.Image);
}
}
else if (e.KeyCode == Keys.Escape)
this.Close();
}
private void pbImage_MouseClick(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
new FullScreenImage(this, thumbnailsSequence, imageFile).ShowDialog();
}
}
}
Class FullScreenImage
This class is a Windows Form shown as a dialog without a FormBorder, occupying the full screen size and having a black background. As such, it gives the optical illusion of being an unusual GUI artifact featuring a full screen mode. It also resizes the image, if it is larger than the full screen dimensions available, while keeping the initial aspect ratio.
namespace ImageFan
{
partial class FullScreenImage : Form
{
private ImageForm parentForm;
private ThumbnailsSequence thumbnailsSequence;
private ImageFile imageFile;
private Image fullScreenImage;
public FullScreenImage(ImageForm parentForm, ThumbnailsSequence thumbnailsSequence, ImageFile imageFile)
{
InitializeComponent();
foreach (Control aControl in this.Controls)
{
aControl.Cursor = this.Cursor;
aControl.Click += new EventHandler(FullScreenImage_Click);
}
this.parentForm = parentForm;
this.thumbnailsSequence = thumbnailsSequence;
ImageSetUp(imageFile);
}
private void ImageSetUp(ImageFile imageFile)
{
if ((pbFullScreenImage.Image != null) && (pbFullScreenImage.Image != GlobalData.InvalidImage))
pbFullScreenImage.Image.Dispose();
if ((fullScreenImage != null) && (fullScreenImage != GlobalData.InvalidImage))
fullScreenImage.Dispose();
this.imageFile = imageFile;
this.fullScreenImage = imageFile.FullSizeImage;
this.Width = Screen.PrimaryScreen.Bounds.Width;
this.Height = Screen.PrimaryScreen.Bounds.Height;
if ((Screen.PrimaryScreen.Bounds.Width >= fullScreenImage.Width) && (Screen.PrimaryScreen.Bounds.Height >= fullScreenImage.Height))
pbFullScreenImage.Image = fullScreenImage;
else
pbFullScreenImage.Image = ImageResizer.CreateResizedFullScreenImageFromImage(fullScreenImage);
}
private void FullScreenImage_KeyDown(object sender, KeyEventArgs e)
{
if ((e.KeyCode == Keys.Escape) || (e.KeyCode == Keys.Enter))
this.Close();
else if ((e.KeyCode == Keys.Space) || (e.KeyCode == Keys.Down) || (e.KeyCode == Keys.Right))
{
if (thumbnailsSequence.NextThumbnail != null)
{
thumbnailsSequence.MoveToNextThumbnail();
ImageSetUp(thumbnailsSequence.CurrentThumbnail.Image);
}
}
else if ((e.KeyCode == Keys.Back) || (e.KeyCode == Keys.Up) || (e.KeyCode == Keys.Left))
{
if (thumbnailsSequence.PreviousThumbnail != null)
{
thumbnailsSequence.MoveToPreviousThumbnail();
ImageSetUp(thumbnailsSequence.CurrentThumbnail.Image);
}
}
}
private void FullScreenImage_Click(object sender, EventArgs e)
{
this.Close();
}
private void FullScreenImage_MouseWheel(object sender, MouseEventArgs e)
{
if (e.Delta < 0)
{
if (thumbnailsSequence.NextThumbnail != null)
{
thumbnailsSequence.MoveToNextThumbnail();
ImageSetUp(thumbnailsSequence.CurrentThumbnail.Image);
}
}
else if (e.Delta > 0)
{
if (thumbnailsSequence.PreviousThumbnail != null)
{
thumbnailsSequence.MoveToPreviousThumbnail();
ImageSetUp(thumbnailsSequence.CurrentThumbnail.Image);
}
}
}
private void FullScreenImage_FormClosed(object sender, FormClosedEventArgs e)
{
this.Dispose();
parentForm.ImageSetUp(thumbnailsSequence.CurrentThumbnail.Image);
}
}
}
Class ImageFile
ImageFile is a flyweight-pattern class, having the image thumbnail stored as its' intrinsic state and the full-sized image as its' extrinsic state, retrieved only on demand.
namespace ImageFan
{
public class ImageFile
: IDisposable
{
private string fileName;
private Image anImage;
public ImageFile(string fileName)
{
this.fileName = fileName;
}
public void GetImageFromFile()
{
this.anImage = this.FullSizeImage;
}
public Image FullSizeImage
{
get
{
try
{
return new Bitmap(fileName);
}
catch
{
return GlobalData.InvalidImage;
}
}
}
public Image Thumbnail
{
get
{
Image thumbnail;
try
{
if (anImage != null)
thumbnail = ImageResizer.CreateThumbnailFromImage(anImage, GlobalData.ThumbnailSize);
else
thumbnail = ImageResizer.CreateThumbnailFromFile(fileName, GlobalData.ThumbnailSize);
}
catch
{
thumbnail = GlobalData.InvalidImageThumbnail;
}
finally
{
if ((anImage != null) && (anImage != GlobalData.InvalidImage))
{
anImage.Dispose();
anImage = null;
}
}
return thumbnail;
}
}
public string ShortFileName
{
get { return fileName.Substring(fileName.LastIndexOf(Path.DirectorySeparatorChar) + 1); }
}
public string LongFileName
{
get { return fileName; }
}
public void Dispose()
{
if ((anImage != null) && (anImage != GlobalData.InvalidImage))
{
anImage.Dispose();
anImage = null;
}
GC.SuppressFinalize(this);
}
~ImageFile()
{
if ((anImage != null) && (anImage != GlobalData.InvalidImage))
anImage.Dispose();
}
}
}
Class ImageFolder
This class extracts and manages the image files within a given folder (directory).
namespace ImageFan
{
public class ImageFolder
{
private List<ImageFile> imageFiles;
private int imageCount;
public ImageFolder(string folder)
{
imageFiles = new List<ImageFile>();
CreateImagesList(folder);
imageCount = imageFiles.Count;
}
private void CreateImagesList(string folder)
{
DirectoryInfo di = new DirectoryInfo(folder);
FileInfo[] filesInformation = di.GetFiles("*", SearchOption.TopDirectoryOnly);
foreach (FileInfo fi in filesInformation)
switch (fi.Extension.ToLower())
{
case ".bmp":
case ".jpg":
case ".jpe":
case ".jpeg":
case ".gif":
case ".tif":
case ".tiff":
case ".png":
imageFiles.Add(new ImageFile(fi.FullName));
break;
}
}
public int ImagesCount
{
get { return imageCount; }
}
public List<ImageFile> ImagesList
{
get
{
return imageFiles;
}
}
}
}
Class ImageResizer
This type features two operations resizing an image extracted from a file (for the generation of thumbnails) and resizing an image taken from memory (for the full-screen mode).
Class GlobalData
This class contains references to the resource images LoadingImage and InvalidImage, as well as to their respective thumbnails.
Lessons Learned
Although this image viewer is a managed application, there is no disparity in browsing or viewing speed (besides specific optimizations) between this solution and native executable ones, such as IrfanView, XnView and AcdSee. This is because the .NET System.Drawing and System.Windows.Forms classes are rather tight wrappers over the WinAPI functionality.
The Image class, inherited by the Bitmap class, implements the IDisposable interface, making the flyweight design pattern implementation straightforward, due to being able to manage the image memory footprint in a deterministic manner.
The improved presence of 64 bit operating systems makes managed programming runtimes truly shine. This is because the applications written in managed languages (such as .NET CLS compliant ones) inherently support a 32 to 64 bit switch using the same binary package, while offering the full advantages of the running platform.
Source Code and Application Download
The complete source code of the ImageFan application (a Google Code project) can be accessed here. If one is only interested in the binaries, they can be downloaded from this link.
I would gladly welcome contributions and feedback to this ImageFan open-source (GPL v3) project.
References
- [1] The Microsoft Developer Network (MSDN) pages
Contact
On my website, http://www.mihnearadulescu.com/.
History
- Version 0.1 - Initial submission - 24/02/2010
- Version 0.2 - Added download links at the head of the article - 28/02/2010
- Version 0.3 - Code optimizations and bug-fixes - 18/04/2010
- Version 0.4 - Updated content, sources and binaries - 21/09/2011
- Version 0.5 - Reengineered code with significant bug fixes - 09/04/2012