Introduction
I've tried a few image viewer utilities out there but couldn't find one that really fits my preference, so I've decided to write up one for my own use. I've got all the functionality I need but would like to get input from experts out there on a few issues.
The Problems with Common Image Utilities
- Load photos as a thumbnail list with no option to switch to simple file name list. This would take forever when the folder contains several hundreds of photos. This is very common when unloading photos taken from a digital camera that has 1GB+ SD card.
- The thumbnail list would reside in a wide view pane that takes up valuable view space for the main image plus the annoying double click to open a photo in the main view, then close and double click on another.
The Utility Features
There are too many features to list but the general idea is to make the photo list as narrow as possible and the main view as large as possible. Selecting a photo would display it in the main view using the default "Fit to screen" so user can see the whole picture without having to scroll right/down. A photo taken from a 6 mega pixels digital camera is typically 2576 x 1932 resolution. Once a photo is selected, the listview has focus and subsequent photo can be viewed by simply pressing the up/down key to select next/previous file.
Useful Imaging Code
static Image ScaleByPercent(Image imgPhoto, int Percent)
{
float nPercent = ((float)Percent / 100);
int sourceWidth = imgPhoto.Width;
int sourceHeight = imgPhoto.Height;
int destWidth = (int)(sourceWidth * nPercent);
int destHeight = (int)(sourceHeight * nPercent);
Bitmap bmPhoto = new Bitmap(destWidth, destHeight,
PixelFormat.Format24bppRgb);
bmPhoto.SetResolution(imgPhoto.HorizontalResolution,
imgPhoto.VerticalResolution);
Graphics grPhoto = Graphics.FromImage(bmPhoto);
grPhoto.InterpolationMode = InterpolationMode.HighQualityBicubic;
grPhoto.DrawImage(imgPhoto,
new Rectangle(0, 0, destWidth, destHeight),
new Rectangle(0, 0, sourceWidth, sourceHeight),
GraphicsUnit.Pixel);
grPhoto.Dispose();
return bmPhoto;
}
static Image CreateThumbnail(Image imgPhoto)
{
Bitmap thumbBmp = new Bitmap(100, 100);
Graphics g = Graphics.FromImage(thumbBmp);
g.FillRectangle(Brushes.White, 0, 0, 100, 100);
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
g.InterpolationMode =
System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
int thumbWidth, thumbHeight;
if (imgPhoto.Width >= imgPhoto.Height)
{
thumbWidth = 100;
thumbHeight = (int)((double)imgPhoto.Height *
(100 / (double)imgPhoto.Width));
}
else
{
thumbHeight = 100;
thumbWidth = (int)((double)imgPhoto.Width *
(100 / (double)imgPhoto.Height));
}
int top = (100 - thumbHeight) / 2;
int left = (100 - thumbWidth) / 2;
g.DrawImage(imgPhoto, new Rectangle(left, top, thumbWidth, thumbHeight),
new Rectangle(0, 0, imgPhoto.Width, imgPhoto.Height),
GraphicsUnit.Pixel);
g.Dispose();
return thumbBmp;
}
private static Image AlterBrightness(Image bmp, int level)
{
if (level == 50)
{
return bmp;
}
Graphics graphics = Graphics.FromImage(bmp);
if (level < 50)
{
int conversion = 250 - (5 * level);
Pen pDark = new Pen(Color.FromArgb(conversion, 0, 0, 0), bmp.Width * 2);
graphics.DrawLine(pDark, 0, 0, bmp.Width, bmp.Height);
}
else if (level > 50)
{
int conversion = (5 * (level - 50));
Pen pLight = new Pen(Color.FromArgb(conversion, 255, 255, 255),
bmp.Width * 2);
graphics.DrawLine(pLight, 0, 0, bmp.Width, bmp.Height);
}
graphics.Save();
graphics.Dispose();
return bmp;
}
Encryption/Decryption Code
private bool EncryptFile(string inputFile, string outputFile)
{
bool bSuccess = false;
if (_CurrentPassword == "") return bSuccess;
try
{
UnicodeEncoding UE = new UnicodeEncoding();
byte[] key = UE.GetBytes(@_CurrentPassword);
string cryptFile = outputFile;
FileStream fsCrypt = new FileStream(cryptFile, FileMode.Create);
RijndaelManaged RMCrypto = new RijndaelManaged();
CryptoStream cs = new CryptoStream(fsCrypt,
RMCrypto.CreateEncryptor(key, key),
CryptoStreamMode.Write);
FileStream fsIn = new FileStream(inputFile, FileMode.Open);
int data;
while ((data = fsIn.ReadByte()) != -1)
cs.WriteByte((byte)data);
fsIn.Close();
cs.Close();
fsCrypt.Close();
bSuccess = true;
}
catch(Exception)
{
}
return bSuccess;
}
private MemoryStream DecryptFile(string inputFile)
{
if (_CurrentPassword == "") return _MSErrorImage;
FileStream fsCrypt = new FileStream(inputFile, FileMode.Open);
try
{
MemoryStream msOut = new MemoryStream();
UnicodeEncoding UE = new UnicodeEncoding();
byte[] key = UE.GetBytes(@_CurrentPassword);
RijndaelManaged RMCrypto = new RijndaelManaged();
CryptoStream cs = new CryptoStream
(fsCrypt, RMCrypto.CreateDecryptor(key, key), CryptoStreamMode.Read);
int data;
while ((data = cs.ReadByte()) != -1)
{
msOut.WriteByte((byte)data);
}
cs.Close();
fsCrypt.Close();
return msOut;
}
catch
{
fsCrypt.Close();
return _MSErrorImage;
}
}
private bool DecryptFile(string inputFile, string outputFile)
{
bool bSuccess = false;
if (_CurrentPassword == "") return bSuccess;
FileStream fsCrypt = new FileStream(inputFile, FileMode.Open);
try
{
FileStream fsOut = new FileStream(outputFile, FileMode.Create);
UnicodeEncoding UE = new UnicodeEncoding();
byte[] key = UE.GetBytes(@_CurrentPassword);
RijndaelManaged RMCrypto = new RijndaelManaged();
CryptoStream cs = new CryptoStream
(fsCrypt, RMCrypto.CreateDecryptor(key, key), CryptoStreamMode.Read);
int data;
while ((data = cs.ReadByte()) != -1)
{
fsOut.WriteByte((byte)data);
}
cs.Close();
fsCrypt.Close();
fsOut.Close();
bSuccess = true;
}
catch
{
fsCrypt.Close();
}
return bSuccess;
}
Partial Thumbnails Creation
For thumbnail rendering, I use an approach similar to Stack's Pop.
- The
Listview
is mapped to an ImageList
in design view. - For thumbnail display mode, set all images in the
ImageList
to a default "Processing" image and store the list of pending (to be created) thumbnails in a custom object list. - The thumbnails creation process is run in a separate Thread. It loops through the list of pending thumbnails and processes 10 at a time (or less if less than 10 remaining), then exits and calls a function that executes in the parent Thread to update the
ImageList
with the newly created thumbnails and refresh the ListView
. The parent Thread function then pops (remove) the first 10 items from the list, checks if there're still pending items and calls the thumbnails creation thread again until there're no more in the Pending list.
Thread _CreateThumbListThread = null;
List<ThumbnailItem> _ThumbList = new List<ThumbnailItem>();
int _MaxThumbPerThreadRun = 10;
private void RunThumbGeneratorThread()
{
if (_ThumbList.Count > 0)
{
_CreateThumbListThread = new Thread(new ThreadStart(CreateThumbList));
_CreateThumbListThread.Start();
}
}
private void CreateThumbList()
{
int NumItemsToProcess = (_MaxThumbPerThreadRun
< _ThumbList.Count ? _MaxThumbPerThreadRun : _ThumbList.Count);
for (int i = 0; i < NumItemsToProcess; i++)
{
if (IsPhoto(_ThumbList[i].FileExtension))
{
Bitmap orgImage;
if (_ThumbList[i].FileFullName.IndexOf("_Enc@@") != -1)
{
GetCurrentConfigEncryptionKey(false);
orgImage = (Bitmap)Bitmap.FromStream
(DecryptFile(_ThumbList[i].FileFullName));
}
else
{
orgImage = (Bitmap)Bitmap.FromFile(_ThumbList[i].FileFullName);
}
_ThumbList[i].ThumbnailImage = CreateThumbnail(orgImage);
}
}
UpdateLargeIcons(NumItemsToProcess);
}
private delegate void UpdateLargeIconsDelegate(int ItemsProcessed);
private void UpdateLargeIcons(int ItemsProcessed)
{
if (listView1.InvokeRequired)
{
UpdateLargeIconsDelegate d = new UpdateLargeIconsDelegate
(UpdateLargeIcons);
listView1.BeginInvoke(d, new object[] { ItemsProcessed });
}
else
{
for (int i = 0; i < ItemsProcessed; i++)
{
thumbImageList.Images[_ThumbList[i].ThumbListIndex] =
_ThumbList[i].ThumbnailImage;
}
listView1.Refresh();
for (int j = 0; j < ItemsProcessed; j++)
{
_ThumbList.RemoveAt(0);
}
if (_ThumbList.Count > 0)
{
RunThumbGeneratorThread();
}
}
}
public class ThumbnailItem
{
Image _ThumbImage = null;
public ThumbnailItem(int ThumbListIndex,
string FileFullName, string FileExtension)
{
this.ThumbListIndex = ThumbListIndex;
this.FileFullName = FileFullName;
this.FileExtension = FileExtension;
}
public int ThumbListIndex
{
get;
set;
}
public string FileFullName
{
get;
set;
}
public string FileExtension
{
get;
set;
}
public Image ThumbnailImage
{
get { return _ThumbImage; }
set { _ThumbImage = value; }
}
Framework Components Used
TreeView
, ListView
, PictureBox
, TabControl
, etc.
Compiler Requirements/Usage Notes
Improvement Desires
- When viewing photos as thumbnail list, the
ListView
is initially loaded with just the file names. A separate thread is called to generate all the thumbnails, and once complete, invoke another function running in the parent thread to update the listview
's LargeIconList
with the generated thumbnails. This prevents the listview
from freezing up while hundreds of thumbnails are being created in the background and allows photo selection to load in main view. I'm looking for a way to generate, say, 10 or 20 thumbnails at a time and make them show in the Listview
and repeat the same process until all are shown. I've tried a few approaches such as thread callback but the display is just too screwy.
2/18/2009 update - Successfully modified to load 10 thumbnails at a time. - The thumbnail creation uses the best setting provided by the .NET library but some photos just won't display as good as the built-in explorer's thumbnail view.
- Efficiency - prevent memory leak, better way to dispose objects/initializing components, etc.
History
- 12th February, 2009: Initial version
- 18th February, 2009: Modified version