|
|||||||||||||||||||||||
|
|||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionThe multi-image viewer is a follow-up on my previous article, the single image viewer. This applet demonstrates:
This applet is part of the prototypes in the Yet Another Photo Organizer (YAPO) open source project hosted by Wdevs here. This code is prototype code intended to explore performance and usability issues. Keep in mind some of it is a bit hacked. Receiving Multiple Files From A Drag-DropThis is very simple. In particular, the application receives only filenames and directory names. You can drop a folder onto the viewer or individual files. The current implementation does recurse into sub-folders. When a drag operation is completed, the application calls protected ArrayList GetFiles(DragEventArgs e)
{
ArrayList files=new ArrayList();
if ( (e.AllowedEffect & DragDropEffects.Copy) == DragDropEffects.Copy)
{
Array data=((IDataObject)e.Data).GetData("FileDrop") as Array;
if (data != null)
{
foreach(string fn in data)
{
string ext=Path.GetExtension(fn).ToLower();
if ( (ext==".jpg") || (ext==".png") || (ext==".bmp") )
{
files.Add(fn);
}
else
{
string[] dirFiles=Directory.GetFiles(fn);
foreach(string fn2 in dirFiles)
{
ext=Path.GetExtension(fn2).ToLower();
if ( (ext==".jpg") || (ext==".png") || (ext==".bmp") )
{
files.Add(fn2);
}
}
}
}
}
}
return files;
}
Why Cast To IDataObject?This is something I discovered after I wrote my single image viewer article. When receiving a file or file list from say, Explorer, Being A Drag-Drop SourceI wanted to be able to drag an image in the multi-image viewer to my already created single image viewer. Based on the code example for the On Mouse DownOn a mouse down event, the image index is obtained and the drag box, which the mouse has to move outside of, is established. private void OnMouseDown(object sender, MouseEventArgs e)
{
if ((e.Button & MouseButtons.Left)==MouseButtons.Left)
{
// ignore SystemInformation.DragSize for now
int col=e.X / panelWidth;
int row=(e.Y+scrollBar.Value) / panelHeight;
int imgIdx=row * cols + col;
if (imgIdx < files.Count)
{
dragImageFilename=(string)files[imgIdx];
Size dragSize=SystemInformation.DragSize;
dragBox=new Rectangle(new Point(e.X - dragSize.Width/2,
e.Y - dragSize.Height/2), dragSize);
dragging=true;
}
}
}
On Mouse UpOn a mouse up event, the dragging flag is cleared. private void OnMouseUp(object sender, MouseEventArgs e)
{
if ((e.Button & MouseButtons.Left)==MouseButtons.Left)
{
dragging=false;
}
}
On Mouse MoveOn the mouse move event, the mouse position is checked to be outside of the minimum drag box window. If it is, the private void OnMouseMove(object sender, MouseEventArgs e)
{
if (dragging)
{
if (!dragBox.Contains(e.X, e.Y))
{
string[] filenames=new string[] {dragImageFilename};
DataObject data=new DataObject(DataFormats.FileDrop, filenames);
((Control)sender).DoDragDrop(data, DragDropEffects.Copy);
dragging=false;
}
}
}
Since I'm only enabling the Optimizing The ViewerThere are a few optimization issues to consider:
Loading The Original Image FilesOnce the drag-drop operation completes, it starts a thread to load the images: private void OnDragDrop(object sender, System.Windows.Forms.DragEventArgs e)
{
files=GetFiles(e);
images.Clear();
loadImagesThread=new Thread(new ThreadStart(LoadImages));
loadImagesThread.Start();
}
private void LoadImages()
{
foreach(string fn in files)
{
try
{
Bitmap bitmap=new Bitmap(fn);
Image image=new Bitmap(bitmap, 256, 256*bitmap.Height / bitmap.Width);
// Dispose of the large image right away rather than waiting
// for the GC to do it.
bitmap.Dispose();
lock(images)
{
images.Add(image);
}
OnSizeChanged(null, EventArgs.Empty);
}
catch(Exception)
{
}
}
}
Using a thread allows the application to start displaying the images as they are converted to thumbnails and the user can also start changing the viewer size, the image size, and dragging images to the single viewer. Of note here is that after the original bitmap is loaded and converted to a thumbnail, I call the The Creating ThumbnailsNotice that after the original image is loaded, a thumbnail is created: Image image=new Bitmap(bitmap, 256, 256*bitmap.Height / bitmap.Width);
that is proportional to the master image dimensions, with the width fixed at 256 pixels across. This seems like a good value that creates presentable images across a variety of resolutions. Resizing images on the fly is very time consuming for large images. I had toyed with the idea of a "smart" algorithm that might create three or so different thumbnails at different sizes. The idea here is that, when a large image is being displayed, there are fewer of them on the screen. Therefore, you can increase the resolution of the thumbnail. Essentially, an algorithm that balances the number of images with the quality of the image. However, I decided not to implement that yet. Using An Owner Draw SurfaceMy original idea was that the viewing surface would be comprised of The owner drawn control is derived from a SetStyle(ControlStyles.DoubleBuffer |
ControlStyles.UserPaint |
ControlStyles.AllPaintingInWmPaint, true);
Paint+=new PaintEventHandler(OnPaint);
On PaintThe private void OnPaint(object sender, PaintEventArgs e)
{
int imgIdx=imgOffset;
Point p=new Point(0, -vOffset);
for (int j=0; j<rows; j++)
{
for (int i=0; i<cols; i++)
{
e.Graphics.DrawRectangle(pen, p.X, p.Y, size.Width, size.Height);
...
The next set of calculations determines the width and height of the image scaled to the current dimensions of the image frame. The image inside the frame has to be proportional to the thumbnail, but it also has to be based on the size that the slider sets. The slider controls the width of the image, thus the height has to be proportional to the width. However, whether the image is in landscape or portrait mode has to be determined. If in portrait mode, the height is "master" (we don't want the height to exceed the image frame because the width is smaller), and in landscape, the width is "master" (the width of the image shouldn't exceed the width of the image frame). This is all done in the following calculations: ...
if ( (images != null) && (imgIdx < images.Count) )
{
Image image;
lock(images)
{
image=(Image)images[imgIdx];
}
++imgIdx;
float fw=size.Width;
float fh=size.Height;
float iw=imgWidth;
float ih=imgWidth * image.Height / image.Width;
// iw/fw > ih/fh, then iw/fw controls ih
// frame width is always >= image width
float rw=fw/iw; // ratio of frame width to image width
float rh=fh/ih; // ratio of frame height to image height
int width;
int height;
// determine which dimension takes precedence
if (rw < rh)
{
width=(int)fw;
}
else
{
width=(int)(iw * rh);
}
// scale width based on the % of the image width is filling the
// frame
width=(int)(width * iw/fw);
// adjust height to maintain aspect ratio
height=width * image.Height / image.Width;
...
Finally, the image should be centered in the image frame, then drawn: int x=(size.Width-width)/2;
int y=(size.Height-height)/2;
// +1 provides better centering
e.Graphics.DrawImage(image, new Rectangle(p.X+x+1, p.Y+y+1,
width, height));
}
...
Usability IssuesOf anything, this probably took the longest time to figure out how to get right. I looked at usability from the perspective of performance and the application "feel". The performance issues are adequately addressed with a couple options to improve performance as a balance between speed and image quality. As to application feel, I discovered that it is driven entirely by the idea that the user can change the image size with the slider control. Image Size And Frame SizeHere's the issue: when the user changes the image size, this directly affects the number of images that can be displayed horizontally. Let's take an extreme example. Given a horizontal width of 300 and an image width of 100, three images (300/100) can be displayed. Now, change the image width to 101 pixels. Now only two images can be displayed, and there's 98 pixels of wasted space. So if we go from three 100 pixel images to two 101 pixel images, what actually needs to happen is three things:
Point number 2 results in some interesting behavior. Let's say that our form size is also 300 pixels. When we go to two frames (because only two images of 101 pixels width can be displayed in a 300 pixel width form), the resulting frames are actually form width / 2 in size, or 150 pixels. This means that our image is 49 pixels smaller than our frame, and as long as the user increases the image size up to 150, the image will grow in the frame, but the frame will remain the same size. Conversely, when we shrink image sizes, the frame remains the same size while the images shrink, until the image size allows more frames to be displayed. Explaining this takes a lot longer than the actual code to calculate it: int imgWidth=tbarImageSize.Value;
// Get the # of columns required to display the images fit to the columns.
// +4 ensures a small margin within the image viewing rectangle.
cols=pnlImages.ClientSize.Width/(imgWidth+4);
if (cols==0)
{
cols=1;
}
// Now get the actual width of the panels, which may be larger.
// Rounding will result in an unused edge on the right of the window.
// We're not going to deal with this minor issue.
panelWidth=pnlImages.ClientSize.Width/cols;
The result is an interesting visual effect, in which the frames take up as much of the form space as possible and the images grow and shrink within the frames. I find this display nicer than leaving large amounts of dead space along one edge, which would happen if images were just displayed from left to right based on image size. It's also nicer than centering images as this creates a strange ballooning expanding/contracting effect. ScrollingThe next thing I wanted to do was to be able to scroll by pixel. This is aesthetically pleasing. On the other hand, when the user pages up or down by clicking on the scrollbar's track, I wanted to only scroll one image row at a time. To accomplish this, I had to override how the LargeChange value works so as to maintain a proper thumb to track height ratio while at the same time allowing sub-page large change and sub-row small changes in scrolling. You can read more about that solution in the article on scrollbars. As to scrolling the owner draw area, the algorithm is designed to display the least amount of images required to display, accounting for rows partially truncated at the top and bottom of the viewer. This is handled very simply by calculating the vertical offset of the viewer modulus the frame height and also the actual image offset in the array for the first partially viewable image: vOffset=scrollBar.Value % panelHeight;
imgOffset=cols * (scrollBar.Value / panelHeight);
Now go back up to the ConclusionThe result is a nice multiple image viewer that you can use as a kind of "test strip" for a folder of images and that supports dragging single images to another application. It makes for a good prototype to iron out all the complexities of a viewer before writing a full-blown application.
|
||||||||||||||||||||||