Click here to Skip to main content
Licence CPOL
First Posted 24 Feb 2010
Views 23,629
Downloads 2,044
Bookmarked 79 times

ImageFan

By | 9 Apr 2012 | Article
A lightweight image viewer for .NET 2.0, supporting multi-core processing

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
{
    
    /// <summary>
    /// Class that partitions a list of tasks to be handled by as many threads as the processor cores available.
    /// </summary>
    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();
        }


        /// <summary>
        /// Builds the list of subfolders of the given folder and displays it within the control.
        /// </summary>
        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
    {
        /// <summary>
        /// Height of the image name label control.
        /// </summary>
        private const int ImageNameHeight = 30;

        /// <summary>
        /// Separator height, separating the label from the image.
        /// </summary>
        private const int SeparatorHeight = 5;


        /// <summary>
        /// The thumbnails sequence parent container.
        /// </summary>
        private ThumbnailsSequence thumbnailsSequence;

        /// <summary>
        /// The index of the thumbnail in the thumbnail sequence of the folder.
        /// </summary>
        private int thumbnailIndex;

        /// <summary>
        /// The image file whose thumbnail is to be displayed.
        /// </summary>
        private ImageFile imageFile;

        /// <summary>
        /// Helper delegate for the thread setting the image thumbnail.
        /// </summary>
        private delegate void SetThumbnailImageThreadDelegate(Image thumbnail);



        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="thumbnailsSequence">The thumbnails sequence parent container</param>
        /// <param name="thumbnailIndex">The index of the thumbnail in the thumbnail sequence of the folder</param>
        /// <param name="imageFile">The image file whose thumbnail is to be displayed</param>
        public ThumbnailBox(ThumbnailsSequence thumbnailsSequence, int thumbnailIndex, ImageFile imageFile)
        {
            InitializeComponent();

            this.thumbnailsSequence = thumbnailsSequence;
            this.thumbnailIndex = thumbnailIndex;
            this.imageFile = imageFile;

            SetLoadingImageThumbnail();
        }


        /// <summary>
        /// Gets or sets the index of the thumbnail in the thumbnail sequence of the folder.
        /// </summary>
        public int ThumbnailIndex
        {
            get { return thumbnailIndex; }
            set { thumbnailIndex = value; }
        }


        /// <summary>
        /// Gets the image corresponding to the thumbnail.
        /// </summary>
        public ImageFile Image
        {
            get { return imageFile; }
        }


        /// <summary>
        /// Thread method that sets the thumbnail image to be displayed.
        /// </summary>
        public void SetThumbnailImageThread()
        {
            Image anImage = imageFile.Thumbnail;

            if (this.InvokeRequired)
                this.Invoke(new SetThumbnailImageThreadDelegate(SetThumbnailImageHelper), anImage);
            else
                SetThumbnailImageHelper(anImage);
        }

        /// <summary>
        /// Thread method that reads the image from disc.
        /// </summary>
        public void ReadImageFromDiscThread()
        {
            imageFile.GetImageFromFile();
        }


        /// <summary>
        /// Helper method for setting the thumbnail image.
        /// </summary>
        /// <param name="anImage">The thumbnail image to be set</param>
        private void SetThumbnailImageHelper(Image anImage)
        {
            pbThumbnail.Image = anImage;
        }


        /// <summary>
        /// Sets the image load logo as thumbnail.
        /// </summary>
        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);
        }


        /// <summary>
        /// Sets the border style of the control.
        /// </summary>
        /// <param name="borderStyle">The border style to be set</param>
        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);
            }
        }


        /// <summary>
        /// Loads the form on the screen.
        /// </summary>
        private void ThumbnailBox_Load(object sender, EventArgs e)
        {
            foreach (Control aControl in this.Controls)
            {
                aControl.Cursor = this.Cursor;
                aControl.MouseClick += new MouseEventHandler(ThumbnailBox_MouseClick);
            }
        }
        
        
        /// <summary>
        /// Creates a new form to display the full-size image corresponding to the thumbnail.
        /// </summary>
        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
{
    
    /// <summary>
    /// Class representing a sequence of thumbnails in a flow layout panel.
    /// </summary>
    public class ThumbnailsSequence : FlowLayoutPanel
    {

        private delegate ThumbnailBox CreateThumbnailBoxDelegate(int i);
        
        /// <summary>
        /// The directory where images reside.
        /// </summary>
        private string currentDirectory;

        /// <summary>
        /// The list of image files within the directory.
        /// </summary>
        private List<ImageFile> imageFiles;

        private TasksDispatcher tasksDispatcher;

        /// <summary>
        /// The currently selected thumbnail.
        /// </summary>
        private ThumbnailBox currentThumbnail;

        /// <summary>
        /// Determines whether or not the navigation through the thumbnails should reset
        /// their border status when navigated to.
        /// </summary>
        private bool allowSetThumbnailBorder;

        private bool pendingAction;


        /// <summary>
        /// Constructor.
        /// </summary>
        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();
        }


        /// <summary>
        /// Adds the new thumbnails to the container control.
        /// </summary>
        private void AddNewThumbnails()
        {
            try
            {
                ImageFolder container = new ImageFolder(currentDirectory);
                imageFiles = container.ImagesList;
            }
            catch { }
        }


        /// <summary>
        /// Removes the thumbnails from the container control.
        /// </summary>
        public void DisposeOfPreviousThumbnails()
        {
            this.currentThumbnail = null;

            foreach (ThumbnailBox thumbnail in this.Controls)
                thumbnail.Dispose();

            this.Controls.Clear();

            foreach (ImageFile anImageFile in imageFiles)
                anImageFile.Dispose();
        }


        /// <summary>
        /// Shows the thumbnails on the screen.
        /// </summary>
        public void ShowThumbnails()
        {
            this.SuspendLayout();
            
            this.DisposeOfPreviousThumbnails();
            this.AutoScrollPosition = new Point(0, 0);
            this.AddNewThumbnails();

            this.ResumeLayout(true);

            this.SetThumbnails();
        }


        /// <summary>
        /// Sets the directory where images reside.
        /// </summary>
        public string CurrentDirectory
        {
            set
            { 
                this.currentDirectory = value;
            }
        }


        /// <summary>
        /// Gets or sets the currently selected thumbnail.
        /// </summary>
        public ThumbnailBox CurrentThumbnail
        {
            get { return currentThumbnail; }
            set { currentThumbnail = value; }
        }


        /// <summary>
        /// Sets the border of the currently selected thumbnail to a given border style.
        /// </summary>
        /// <param name="borderStyle">The border style to be applied to the current thumbnail</param>
        private void SetThumbnailBorder(BorderStyle borderStyle)
        {
            if (allowSetThumbnailBorder == true)
            {
                if (currentThumbnail != null)
                {
                    currentThumbnail.SetBorder(borderStyle);
                    currentThumbnail.Select();
                }
            }
        }


        /// <summary>
        /// Moves to the given thumbnail in the sequence.
        /// </summary>
        public void MoveToThumbnail(ThumbnailBox thumbnail)
        {
            if (thumbnail != null)
            {
                SetThumbnailBorder(BorderStyle.None);
                currentThumbnail = thumbnail;
                SetThumbnailBorder(BorderStyle.Fixed3D);
                ScrollControlIntoView(currentThumbnail);
            }
        }

        /// <summary>
        /// Moves to the next thumbnail in the sequence, if any.
        /// </summary>
        public void MoveToNextThumbnail()
        {
            MoveToThumbnail(this.NextThumbnail);
        }

        /// <summary>
        /// Moves to the previous thumbnail in the sequence, if any.
        /// </summary>
        public void MoveToPreviousThumbnail()
        {
            MoveToThumbnail(this.PreviousThumbnail);
        }


        /// <summary>
        /// Moves to the upper thumbnail in the sequence, if any.
        /// </summary>
        public void MoveToUpperThumbnail()
        {
            MoveToThumbnail(this.UpperThumbnail);
        }

        /// <summary>
        /// Moves to the lower thumbnail in the sequence, if any.
        /// </summary>
        public void MoveToLowerThumbnail()
        {
            MoveToThumbnail(this.LowerThumbnail);
        }

        /// <summary>
        /// Moves to the left thumbnail in the sequence, if any.
        /// </summary>
        public void MoveToLeftThumbnail()
        {
            MoveToThumbnail(this.LeftThumbnail);
        }

        /// <summary>
        /// Moves to the right thumbnail in the sequence, if any.
        /// </summary>
        public void MoveToRightThumbnail()
        {
            MoveToThumbnail(this.RightThumbnail);
        }



        /// <summary>
        /// Gets the next thumbnail in the sequence.
        /// </summary>
        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;
                }
            }
        }

        /// <summary>
        /// Gets the previous thumbnail in the sequence.
        /// </summary>
        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;
                }
            }
        }


        /// <summary>
        /// Gets the upper thumbnail in the visual representation of the sequence.
        /// </summary>
        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;
                }
            }
        }

        /// <summary>
        /// Gets the lower thumbnail in the visual representation of the sequence.
        /// </summary>
        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;
                }
            }
        }

        /// <summary>
        /// Gets the left thumbnail in the visual representation of the sequence.
        /// </summary>
        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;
                }
            }
        }

        /// <summary>
        /// Gets the right thumbnail in the visual representation of the sequence.
        /// </summary>
        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
{
    
    /// <summary>
    /// Shows a full-sized image in a form.
    /// </summary>
    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
{
    
    /// <summary>
    /// Flyweight pattern image file class.
    /// </summary>
    public class ImageFile
        : IDisposable
    {
        
        /// <summary>
        /// The name of the image file.
        /// </summary>
        private string fileName;

        /// <summary>
        /// The image extracted from the file.
        /// </summary>
        private Image anImage;


        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="fileName">The name of the image file</param>
        public ImageFile(string fileName)
        {
            this.fileName = fileName;
        }


        /// <summary>
        /// Gets the image from the file.
        /// </summary>
        public void GetImageFromFile()
        {
            this.anImage = this.FullSizeImage;
        }

        /// <summary>
        /// Gets the full-sized image corresponding to an image file.
        /// </summary>
        public Image FullSizeImage
        {
            get
            {
                try
                {
                    return new Bitmap(fileName);
                }
                catch
                {
                    return GlobalData.InvalidImage;
                }
            }
        }

        /// <summary>
        /// Gets the thumbnail image corresponding to an image file.
        /// </summary>
        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;
            }
        }

        /// <summary>
        /// Gets the file name (without path) corresponding to the image.
        /// </summary>
        public string ShortFileName
        {
            get { return fileName.Substring(fileName.LastIndexOf(Path.DirectorySeparatorChar) + 1); }
        }


        /// <summary>
        /// Gets the file name (with path) corresponding to the image.
        /// </summary>
        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
{
    
    /// <summary>
    /// Class representing a folder that contains images.
    /// </summary>
    public class ImageFolder
    {
        
        private List<ImageFile> imageFiles;
        private int imageCount;


        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="folder">The folder containing images.</param>
        public ImageFolder(string folder)
        {
            imageFiles = new List<ImageFile>();
            CreateImagesList(folder);
            imageCount = imageFiles.Count;
        }


        /// <summary>
        /// Creates the list of image files.
        /// </summary>
        /// <param name="folder">The folder where the images reside</param>
        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;
                }
        }


        /// <summary>
        /// Returns the number of images in the folder.
        /// </summary>
        public int ImagesCount
        {
            get { return imageCount; }
        }


        /// <summary>
        /// Returns the images list of the folder.
        /// </summary>
        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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Mihnea Rădulescu

Software Developer
STRATEC Biomedical Systems
Romania Romania

Member

Software engineer

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board. (secure sign-in)
 
Search this forum  
 FAQ
    Noise  Layout  Per page   
  Refresh
QuestionMy vote of 5 PinmemberKendoTM19:39 16 Apr '12  
AnswerRe: My vote of 5 PinmemberMihnea Rădulescu5:16 17 Apr '12  
GeneralMy vote of 5 PinmemberSergio Andrés Gutiérrez Rojas8:26 21 Sep '11  
GeneralI like images, and I wrote something like this some time ago for my Msc PinmvpSacha Barber23:11 18 Apr '10  
GeneralRe: I like images, and I wrote something like this some time ago for my Msc PinmemberMihnea Radulescu0:59 19 Apr '10  
GeneralRe: I like images, and I wrote something like this some time ago for my Msc Pinmemberrctaubert12:55 10 Apr '12  
GeneralRe: I like images, and I wrote something like this some time ago for my Msc PinmvpSacha Barber20:18 10 Apr '12  
GeneralRe: I like images, and I wrote something like this some time ago for my Msc Pinmemberrctaubert2:14 11 Apr '12  
Generalmy vote of 3 PinmemberMP3Observer0:02 3 Mar '10  
GeneralRe: my vote of 3 PinPopularmemberMihnea Radulescu1:36 3 Mar '10  
GeneralRe: my vote of 3 Pinmemberhardsoft9:07 18 Apr '10  
GeneralRe: my vote of 3 PinmvpEddy Vluggen14:51 16 May '10  
GeneralMy vote of 2 PinmemberParadoxon1018:23 28 Feb '10  
GeneralRe: My vote of 2 PinPopularmemberMihnea Radulescu1:42 3 Mar '10  
GeneralCode... PinmemberGary Noble6:19 24 Feb '10  
GeneralRe: Code... PinmemberMihnea Radulescu9:32 24 Feb '10  

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Mobile
Web01 | 2.5.120517.1 | Last Updated 9 Apr 2012
Article Copyright 2010 by Mihnea Rădulescu
Everything else Copyright © CodeProject, 1999-2012
Terms of Use
Layout: fixed | fluid