Introduction
A picture tray is a virtual set of tracks along which your images rotate on the screen. The original version of this project, Rotating Picture Tray, which I wrote and published about a year ago, still works well, but has a few limitations which this version addresses. The most important problem with the original was its difficulty of use. Programming with it was simply not simple enough. Also, the most glaring difference, the old version restricted the motion of the images to a circular path, and though it was still a flashy way to look at photos, most programmers would like the flexibility of adjusting not only the size and height/width ratio of the circle to make it into any ellipse of variable dimension, but also have the ability to turn that ellipse in any direction and then pick whatever way works for the front of the tray. By this, I mean that the old version's rotated images were large in the bottom of the screen, where the illusion made them appear to be closer than the rest which were shrunken near the top. This version allows you to do all that and to point the front wherever you like.
How to use the code
To use the class is really quite simple. First, you must add a reference to it in your IDE. Then, create an instance of a picture tray, and add it to your form's controls, and it is ready to go. Here's a sample code which will do just that without a fuss:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
PicTray.classPicTray_V2 Tray = new PicTray.classPicTray_V2();
public Form1()
{
InitializeComponent();
Controls.Add(Tray);
Tray.Dock = DockStyle.Fill;
PictureBox pic = new PictureBox();
string[] strFiles
= System.IO.Directory.GetFiles("c:\\records\\pictures\\",
"VanessaParadis*.*");
foreach (string filename in strFiles)
{
pic.Load(filename);
Tray.addImageToTray(pic.Image);
}
}
}
}
If you've been using the old version, you may notice that you're no longer passing picture boxes, but just the images when adding them to the tray. The reason for that is also the main difference between the way this version and the original were made. The original version rotated actual picture boxes around the screen, required much memory, and really did not give the best results, while this version paints one bitmap onto the object with the different images where they belong. Performance and quality are much improved even without mentioning the added flexibility of this newer version.
Speaking of which...
What's an ellipse, anyway?
We all know what a circle is, but what's an ellipse? But you see, the question shouldn't be 'what's an ellipse', because really, it's the circle that's so strange since the circle is just a rare kind of ellipse that has a width-radius equal to its height-radius, or in mathematical terms, two identical foci. But I won't bore you with geometry too much, instead, let's have a look at the mechanics of this project since there's a bit of information you'll need before you start programming it into your applications. That is, only if you want to get your hands just a little bit dirty because the class is ready to go as is without any effort.
If you're still reading this, it's probably because you aspire to greater things.
So, let's get to it!
An ellipse is like a circle that's been flattened in either the vertical or the horizontal direction. It's still symmetrical about either center lines, but not equal in width or height. This program not only allows you to make your ellipse whatever breadth you like, tall or short, skinny or fat, hell, you can even make it into a circle if you like. To do this, you have two basic ellipse adjustment parameters: EllipseRadius_Width
and EllipseRadius_Height
. And, they are exactly lwhat they sound like they should be, so if you pull out your calculator and do a bit of trigonometry for a minute, you can look at these lines of code which place the image on an ellipse about the origin as an intermediate step in locating that same image somewhere on the output bitmap.
Point ptTemp
= new Point((int)(dblEllipse_Width
* Math.Cos(Tray_Reordered[intCounter].rotatedAngle))
- Tray_Reordered[intCounter].sz.Width / 2,
(int)(dblEllipse_Height
* Math.Sin(Tray_Reordered[intCounter].rotatedAngle))
- Tray_Reordered[intCounter].sz.Height / 2);
You can see the private versions of the public property names mentioned above, here called 'dblEllipse_Width
' and 'dblEllipse_Height
', being multiplied by Cos and Sin, respectively, to obtain the Cartesian (x, y) coordinates of this image's position on an ellipse about the origin.
This is done in as intermediate step in refreshing the screen during one clock-cycle. The way the whole tray works is very similar to the older version, but we can go over it again anyway. First, all the images are stored in an array of type rotatingTray_PicBox
, as shown below:
public class rotatingTray_PicBox
{
public double angleOnTray;
public double rotatedAngle; public double dblAngularDistanceFromFront;
public short shoDirToFront; public double aspectRatioYoverX;
public string caption;
public bool FlipV; public bool FlipH; public Point pt; public Size sz; public int Index; public Image Image;
}
Many of these fields can be easily explained simply by looking at their names like, caption
, pt
, sz
, and image
, but you might wonder what shoDirToFront
and dblAngularDistanceFromFront
mean. The short variable 'Direction to Front' is calculated and reset every clock cycle, and it tells the program which direction the tray needs to rotate if the user clicks this image and wants to bring it to the front by the shortest route possible. The other mystery variable is a double
variable called 'Angular Distance From the Front'. As I mentioned above in the intro to this article, one major drawback to the previous iteration of this project was its lack of flexibility, because it was hard-coded to face the bottom of the screen. Since my first objective in creating this newer version was to allow the programmer to choose what side of the tray is the front, for this reason, an alternate means of calculating which image is closest to the front, and by how much, had to be used (because the last version was still counting pixels!).
Whichever image was closest to the bottom is the image closest to the front. But in this game, we're dealing with a variable front, and can't line ourselves up with the bottom edge of the screen anymore. Now you're playing with the big boys! So to do this, we keep all angles between FrontAngle
and FrontAnglePlusTwoPi
[FrontAngle, FrontAngle + 2π)
.
void cleanAngle(ref double angle)
{
while (angle < dblFrontAngle)
angle += (Math.PI * 2.0);
while (angle >= dblFrontAngle + Math.PI * 2.0)
angle -= (Math.PI * 2.0);
}
I should give you a brief synopsis of what happens at refresh:
- An array of pointers of identical size as the
Tray[]
array is created.
- The algorithm cycles through each entry in a preliminary evaluation during which each pointer in the new array is linked to the original, and every entry has its new angular distance to the front calculated. This value can range between [0, 2π) so we subtract 2π, and now it ranges between [-π, π).
This step is shown below:
Tray_Reordered = new rotatingTray_PicBox[Tray.Length];
for (int intCounter = 0; intCounter < Tray.Length; intCounter++)
{
Tray[intCounter].rotatedAngle
= cleanAngle(dblAngle
+ Tray[intCounter].angleOnTray);
double dblDistance = Tray[intCounter].rotatedAngle
- dblFrontAngle
- Math.PI;
double dblDiff = Tray[intCounter].rotatedAngle - dblFrontAngle;
Tray[intCounter].shoDirToFront = (short)(dblDiff > Math.PI ? 1 : -1);
Tray[intCounter].dblAngularDistanceFromFront = Math.Abs(dblDistance);
Tray_Reordered[intCounter] = Tray[intCounter];
Tray[intCounter].Index = intCounter;
}
- Next, we use a Quick-Sort to reorder the array of pointers from farthest to nearest so that when we scan the pointer array and we can draw the images farthest in the back first and they can then be drawn over by the images that are closer to the front.
- The following step is the process of actually drawing the images on the screen at the location and size which their position on the ellipse dictates. Doing this requires several steps:
- After the quick-sort, the temporary array of pointers now has a list that runs from the furthest in the back of the tray at index zero to the nearest at the front of the tray, last in the array. So we can scan through this entire array of pointers in-order and, first calculate their size, given their distance from the front.
- We then find the location this image would be if we wanted to place it on an unrotated ellipse centered at the origin using the sin/cos functions and the
radiusWidth
and radiuHeight
variables shown above.
- Then, to rotate this ellipse by an angle equal to
dblEllipseAngle
, we need to translate these Cartesian coordinates into radial coordinates, rotate the radial coordinates, then re-convert the resultant rotated positions into their new Cartesian locations before drawing them on the screen. Having located the unrotated ellipse at the origin makes calculating the radius simpler. To place an image on the screen, we use the rotated radial coordinates, then move that distance away from the center of the ellipse, and draw the image on the screen there.
- You'd think that that would be it, wouldn't you? But, an extra feature this program has is the ability to flip the entire output either horizontally or vertically. Doing this is simply a matter of calling the .NET native bitmap function
RotateFlip()
just before putting the image on the screen:
switch (mirror)
{
case enuMirror.horizontal:
bmp.RotateFlip(RotateFlipType.RotateNoneFlipX);
break;
case enuMirror.vertical:
bmp.RotateFlip(RotateFlipType.RotateNoneFlipY);
break;
case enuMirror.both:
bmp.RotateFlip(RotateFlipType.RotateNoneFlipXY);
break;
}
But if we do that we're going to see a mirror image of what we originally intended. This may be fine in some applications, but if you're dealing with text, the horizontal flip will be a misery to read and the vertical flip will nearly always be a problem. So first, we have to do the same thing to the images we paste onto the bitmap before flipping the whole output and righting them again. We could make a copy of the original image stored in the Tray[]
array, but that would slow things down! So instead, what we do is we keep two boolean variables, one for the vertical flip and one for the horizontal flip (you may have noticed these in the rotatingTray_PicBox
class above). These booleans keep track of which flip operations have currently been effected on the original images so that if we need a vertical flip and we see that the image is already inverted by testing the boolean flipV
variable, then we just plug it on the screen as is. If it is not what we want, then we flip it again and reset the boolean to reflect this change.
Have a look at the code below ...
if (((mirror == enuMirror.vertical || mirror == enuMirror.both)
&& !Tray_Reordered[intCounter].FlipV)
||
!(mirror == enuMirror.vertical || mirror == enuMirror.both)
&& Tray_Reordered[intCounter].FlipV)
{
Tray_Reordered[intCounter].Image.RotateFlip(RotateFlipType.RotateNoneFlipY);
Tray_Reordered[intCounter].FlipV = !Tray_Reordered[intCounter].FlipV;
}
if ((((mirror == enuMirror.horizontal || mirror == enuMirror.both)
&& !Tray_Reordered[intCounter].FlipH)
||
!(mirror == enuMirror.horizontal || mirror == enuMirror.both)
&& Tray_Reordered[intCounter].FlipH))
{
Tray_Reordered[intCounter].Image.RotateFlip(RotateFlipType.RotateNoneFlipX);
Tray_Reordered[intCounter].FlipH = !Tray_Reordered[intCounter].FlipH;
}
So here, you can see that we need to separate the operations into V and H flips. The first if
statement takes care of the vertical flip by testing if it needs a flip and the image is not yet flipped, or if it needs a regular unflipped image and the original image in memory is already flipped; in either of these two cases, it flips the image and toggles the boolean.
But how to use all that flexibility?
If the last version of this program was a nightmare to use, then this one was more so. Bleah. It was horrible (without the graphic editor, that is!). I could sit there at my desk tinkering with variables and fiddling with it for hours and still not get what I was looking for.
Something had to be done...
The image above might help you when you start using the editor (and then imagine trying to do that without the editor!). First, you need to know that the EllipseAngle
is seen solely on the screen. Internally, the ellipse is unrotated so that all calculations are done relative to the x-y plain and then rotated when placed on the screen. This is only important when you're trying to figure out where the front goes because the front angle is relative to the ellipse angle as if the ellipse angle were zero and ran along the positive x axis. Then, you need to remember that the first quadrant out of your trigonometry book doesn't apply to the computer screen, which is a problem because someone a long time ago decided to put the origin at the top left of the screen, making your sin()
function wrong, not half the time, but all the time! So you'd either have to add a minus sign in front of it before using it, or just get used to the fact that the angle on paper is wrong when you look at it on the screen.
Thankfully, the graphical interface is really quite intuitive. The scroll bars allow you to control many of the parameters including the tray's dimensions, the min/max values of the width or height of the images, with textboxes that are activated by the ENTER key to manually enter values. Writing the name (and pressing ENTER!) in the 'name' field will help you later when you go to use the C#Code option. As for the other fields and modes shown above, most of them are self-explanatory. Perhaps, the 'Front Angle User Set Mode' needs some mention since this is an odd feature. You can allow the user to decide which end of your ellipse is the front. Doing this with an unrotating crowded circle gives you a cool effect similar to filing through a rotary tray as the mouse is moved over the object, or you can force the user to click on it in order to turn the front of the tray in that direction.
In this image, you have the choice to allow your Tray to resize itself in any of eight directions, or you can play around with the manual settings to turn and resize it yourself. The advantage here is quite an important one since letting it resize itself can ease your conscience when you know the form is going to fluctuate in size anyway, but if you know the dimensions are static, then you can fine tune it to whatever you like.
Given all the variables involved and the mechanics at play, you can imagine how difficult it can get to achieve exactly what you had envisioned by going back and forth between code and execution until you finally start getting somewhere with your Tray. This is not true for all configurations, but a graphic interface here is a great advantage anyway.
Now, imagine staring at the photos below as they scrolled idly past your eyes on a pretty parade, and then you wanted to include the same thing into your application but didn't want to go through the hassle of taking notes and writing it all on paper before putting it back together again. What would be the use of the editor?
When the ellipse is as it should be, you press a button and the code to make it so auto-magically appear at your fingers tips. And! with a quick click and a paste, this new self-created code is scrawled neatly inside your IDE.
You don't believe me? Have a look at this:
Just right! Right.
But, writing all the parameters involved in setting it so, is tedious! Luckily, the third tab of your editor controls has the C# code already written, and here's where a quick click'n-a-paste writes all the code for you.
And, here's the same thing as it appears in the IDE:
#region "init tray : cLibPicTray"
{
cLibPicTray.Options.BackColor = Color.FromArgb(255, 0, 0, 0);
cLibPicTray.Options.Mirror = PicTray.enuMirror.none;
cLibPicTray.Options.PopUpClickPic = false;
cLibPicTray.Options.SelectionMode = PicTray.enuSelectionMode.click;
cLibPicTray.Options.TrayCenter = new Point(90,290);
cLibPicTray.Options.Width = 1320;
cLibPicTray.Options.Height = 720;
cLibPicTray.Options.Rotation.StepSize = 0.0109955742875643;
cLibPicTray.Options.Rotation.Interval = 1;
cLibPicTray.Options.Rotation.Mode = PicTray.enuRotationMode.toSelected;
cLibPicTray.Options.Ellipse.Angle = 1.20951317163207;
cLibPicTray.Options.Ellipse.Radius_Height = 67;
cLibPicTray.Options.Ellipse.Radius_Width = 235;
cLibPicTray.Options.Front.Angle = 0;
cLibPicTray.Options.Front.UserSet = PicTray.enuFrontUserSet.none;
cLibPicTray.Options.ImageSize.Height_MinMax = new PicTray.classMinMax(5,250);
cLibPicTray.Options.ImageSize.Width_MinMax = new PicTray.classMinMax(75,300);
cLibPicTray.Options.Caption.Show = true;
cLibPicTray.Options.Caption.Font
= new Font("Microsoft Sans Serif",
(float)8.25,
FontStyle.Regular);
cLibPicTray.Options.Caption.ForeColor = Color.FromArgb(255, 255, 255, 0);
cLibPicTray.Options.Caption.BackColor = Color.FromArgb(255, 0, 0, 0);
}
#endregion
You'll still need to add the images yourself, but other than that, you're done!