Click here to Skip to main content
Click here to Skip to main content

Rotating Picture Tray

By , 17 Nov 2009
 

Introduction

This article shows you how to use C# to display pictures on a rotary tray. With this bit of code, you'll be able to quickly throw a bunch of pictures on the screen and allow the user to rotate the tray to view the ones selected, or even click on an image and have it pop up on a separate form. Imagine an invisible rotating tray you can place anywhere on the screen, rotate at any velocity, or resize however you like. Then, imagine pinning images onto this tray and having them shrink behind other images as they move to the back, or stretch out and appear closer as they move to the front. Can't imagine it? Well, now you don't have to, because this code will do it for you.

We're not going to solve any economic crisis or save any baby seals here. All we have is an easy to use Windows object which will lend your projects a keener edge and a bit of zip.

Here's a Screenshot...

Pretty, eh?

Using the Code

You'll need to download and compile both the cLibPicTray and the cLibPicBoxViewer (which I described in a previous article), then include their DLLs in your own project. Listed below is a sample code of how to use this program. It assumes you have a C# Windows form and have already added a button called 'btnAddPicture'.

namespace formTestCLibPicTray
{
    public partial class formTestCLibPicTray : Form
    {
        /// <summary>
        ///  be sure to include the two DLLs : cLibPicBoxViewer + cLibPicTray
        ///  before running this program.
        /// </summary>

        cLibPicTray.cLibPicTray picTray = new cLibPicTray.cLibPicTray();

        public formTestCLibPicTray()
        {
            InitializeComponent();
            Controls.Add(picTray.picBackground);
            picTray.picBackground.Dock = DockStyle.Fill;

            btnAddPicture.Click += new EventHandler(btnAddPicture_Click);
        }

        void btnAddPicture_Click(object sender, EventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog();
            if (ofd.ShowDialog() == DialogResult.OK)
            {
                PictureBox thisPicBox = new PictureBox();
                thisPicBox.Load(ofd.FileName);
                picTray.addPicBox(ref thisPicBox);
            }
        }
    }
}

If we look at the button's event handler for the 'click' event, you'll notice that we first created an OpenFileDialog called ofd, then we only use the results if, after 'showing' this object to the user, the user has pressed the 'OK' button with the mouse, which means that a file has been selected. Next, we create a PictureBox and load it with the image the user selected, and finally, we tell the picture tray to add this picture to itself by calling its .addPicBox() method and sending our newly created PictureBox along using the 'ref' (by reference parameter) keyword.

And, that's it.

If you want to use the formTestCLibPicTray.zip file, you'll find that all the code is wrapped up into that one file which you can download and execute with no preamble. But, if you want to use the DLL files, you'll need to go through it one step at a time. That is,

  1. Download all zip files.
  2. Open and compile the PicBoxViewer into a DLL.
  3. Open the PicTray solution in your C# integrated development environment, and include the DLL file from your PicBoxViewer directory (which you compiled in step #2 above) into this project, and only then will you be able to compile the PicTray into a DLL of its own.
  4. Now that you have these two DLLs, you can make use of the whole she-bang into any project you want!

The PicViewer DLL can run by itself but the PicTray requires PicViewer, so when you're running the PicTray, you'll have to include both of the dynamic link libraries.

There are a few options available to you, the easiest ones are:

picTray.bolPopUpClickedPic = true;
picTray.bolMouseMoveOverAutoSelect = true;
picTray.setBackGroundImage(ofd.FileName);

If you want to have a copy of a picture appear in a separate popup form when the user clicks it, then you set your picTray's bolPopUpClickedPic to true. To have the image which the user's mouse scans over be the selected image without having to click it, then you set the bolMouseMoveOverAutoSelect to true, and to have an image appear behind the others on the background picturebox, you send an image, filename, or URL to the setBackGroundImage method.

There are other variables you can play with. You can move the tray around on the background, set max/min size of the pictures as they move from front to back, and vice-versa, as well as set options to resize the tray's radius. If you feel like fiddling around with it, just write the name of your picTray object and let the intellisense give you the options. Then, if you want to reset everything back to standard parameters, there's a function that'll do that too.

Points of Interest

Are you interested in knowing how it works?

public struct udtTrayLoc
{
    public double angle;
    public double x;
    public double y;
}

public struct udtPicBoxTrayElement
{
    public PictureBox picBox;
    public udtTrayLoc trayLoc;
    public double dblXOverYRatio;
}

public udtPicBoxTrayElement[] udrPicBoxTrayElements;

The array udrPicBoxTrayElement of the user-defined type udtPicBoxTrayElement is your friend. Here it is in a nutshell:

  1. Each element in this array holds the information for each picture in the tray, the picBox, and the picture it contains:
    • Information about where on the tray that picture is located.
    • The aspect-ratio of the image (width over height).
  2. The pictures are evenly distributed around an invisible circular tray.
  3. When the tray is rotated and repositioned, all the images are ranked and placed on the screen from back to front, resizing them from the smallest in the back to the biggest in the front.

Let's look at the Add Picture function:

public void addPicBox(ref PictureBox PicThisBox)
{
    if (udrPicBoxTrayElements == null)
        udrPicBoxTrayElements = new udtPicBoxTrayElement[1];
    else
        Array.Resize<udtpicboxtrayelement>(ref udrPicBoxTrayElements,
                udrPicBoxTrayElements.Length + 1);
            
    udrPicBoxTrayElements[udrPicBoxTrayElements.Length - 1].picBox = PicThisBox;
    picBackground.Controls.Add(
       udrPicBoxTrayElements[udrPicBoxTrayElements.Length - 1].picBox
                             );

    PicThisBox.SizeMode = PictureBoxSizeMode.AutoSize;
    udrPicBoxTrayElements[udrPicBoxTrayElements.Length - 1].dblXOverYRatio
               = (double)udrPicBoxTrayElements[
                                   udrPicBoxTrayElements.Length - 1
                                              ].picBox.Width 
               / (double)udrPicBoxTrayElements[
                                   udrPicBoxTrayElements.Length - 1
                                              ].picBox.Height;

    PicThisBox.SizeMode = PictureBoxSizeMode.StretchImage;
    handle_SizeChange();
    intSelected = udrPicBoxTrayElements.Length - 1;
    repositionTray();
    handle_newSelection();
}

First we create a new entry in our tray elements array by resizing it (or initializing it if it is empty). Then, we store the newly added picture box we received in the function's parameters into this new array element's field called '.picBox' and add it to the picBackground using the Controls.Add function.

Once that's done, we need to do a bit of math and get a workout figuring what the image's aspect ratio is. To do this, we set the PictureBox's mode to 'AutoSize'; this will cause the picture box to stretch itself out to its natural proportions. Then, we force the conversion of the picture's width and height values to doubles, divide them to get their ratio, and store this value in the dblXOverYRatio field of our array element. The line of code looks a bit complicated because it stretches over a few lines, but if you look at it closely, it is really just the width divided by the height of the same picture box.

Once we have this value figured out, we reset the SizeMode to StretchImage so that we can now decide for ourselves just what size we want it to be. Remember that the image will now stretch to whatever size the PictureBox is. This means that if you neglect to retain the image's aspect ratio, the image will appear distorted.

Ladies and Gentlemen, the one and only repositionTray() function:

So, here's where the brunt of the work is done. Whenever we add a picture, delete a picture, resize the background image, or rotate the tray, repositionTray() is there.

We have a variable of type 'double' called 'dblCurrentAngle'. This is the angle at which the tray is rotated at. Remember that each picture has a 'location on the tray' that I mentioned above? Well, when we add/remove a picture, the pictures on the tray all make room, shove elbows sort of speak, and reposition themselves around 2 pi radians of our circle so that they're all evenly distributed around it. Then, given their location on the tray and the tray's rotation, the function repositionTray() calculates where they appear on the rotated tray, and then they are each reassigned new Cartesian coordinates (x, y). Those with negative x values are to the left, and the rest are on the right; those with negative y values are at the bottom of our unit circle and the others are on top.

Once all the pictures have been placed on this imaginary circle, they are ranked using a bubble-sort in the getRankingIndices() function which returns an array of integers. These integers correspond to the indices of the array elements (pictures) ranked from back to front.

Here's a look at the part of this famous function which actually repositions the pictures on the screen after we've already gotten the ranked indices.

for (int intPicCounter = 0; 
           intPicCounter < 
           intRankingIndices.Length; 
           intPicCounter++)
{
    /// -   set their size accordingly (back is smaller, front is bigger) 
    double dblSizeAttenuationFactor; /// -1 is at the front, 1 is at the back
    double dblDistanceToFront = 1
                            + udrPicBoxTrayElements[
                                  intRankingIndices[intPicCounter]
                                                   ].trayLoc.y;
    // dblDistanceToFront ranges from 0(front) to 2(back)
    double dblFactorRange = 1 - dblMinSizeFraction;
    dblSizeAttenuationFactor = 1 
                           - dblFactorRange * (dblDistanceToFront / 2);
    resizePic(ref udrPicBoxTrayElements[
                      intRankingIndices[intPicCounter]
                                     ],
            attenuateUdtCartesian(udrMaxSize, dblSizeAttenuationFactor));

    /// -   place them on the form (left,top)
    ///  - center the x value of pic loc on circle Pt, 
    ///                 place top of pic on circle Pt
    udrCirclePt.x = udrCenter.x 
                + dblCircleRadius * Math.Cos(udrPicBoxTrayElements[
                                        intRankingIndices[intPicCounter]
                                                                  ]
                             .trayLoc.angle + dblCurrentAngle - Math.PI / 2);
    udrCirclePt.y = udrCenter.y 
                - dblCircleRadius * Math.Sin(udrPicBoxTrayElements[
                                        intRankingIndices[intPicCounter]
                                                                  ]
                             .trayLoc.angle + dblCurrentAngle - Math.PI / 2);
    udrPicBoxTrayElements[intRankingIndices[intPicCounter]].picBox.Left 
     = (int)(udrCirclePt.x - udrPicBoxTrayElements[
                                     intRankingIndices[intPicCounter]
                                                  ].picBox.Width / 2);
    udrPicBoxTrayElements[intRankingIndices[intPicCounter]].picBox.Top 
     = (int)(udrCirclePt.y);
    
    /// -   bring to front (will be covered by those that follow)
    udrPicBoxTrayElements[intRankingIndices[intPicCounter]].picBox.BringToFront();
}

We already know in what order to put the images onto the screen so that the ones in the back appear behind the ones in the front. And, we already know where they are on the unit circle, so finding the corresponding location on the screen isn't too difficult. What we need to worry about is the size of the image, and this is a function of how close to the front it is and what the max/min picture sizes are.

On our unit circle, the images at the bottom of the circle are at or close to y= -1, so we can use this knowledge when we calculate dblDistanceToFront and add 1 to the y value. This gives us that picture's distance from the front, which is anything between 0 and 2, such that those at 0 are closest to the front and the pictures 2 'units' from the front are furthest in the back. The dblMinSizeFraction variable is used to determine the size of the picture at the back (smaller) relative to the maximum size of the picture at the front. We use the distance from the front and the factorRange to 'attenuate' the image to the appropriate size. Since each image is not necessarily the same shape or size as the others and the tray needs to accommodate all shapes and sizes, this program uses the maximum values in which the image can be allowed to fit. Given these confinements, the function resizePic() uses the image's aspect ratio which we recorded earlier, and fits the image inside the allowed space without distorting its shape.

Next, we need to place the images on the screen. To do this, imagine that you're sticking a thumbtack through the middle of the top edge of your picture into some spot on your rotating tray. You'll need to dig up your trigonometry a bit and remember that Cos(theta) is the x value on the unit circle at the angle theta, and Sin(theta) is the y value at the angle theta on that same unit circle. You calculate these values remembering that your angle theta in this case is the sum of the picture's location on the tray plus the tray's angle of rotation and an extra Pi/2 for good luck (actually, that extra pi/2 places the angle zero at the bottom of the screen for appearance's sake). Then, multiply these values by the output circle's radius and add them to that circle's center.

Confused? Don't be. You've got a central location. You have a tray that rotates, and you have points on that tray; the rest is more math than programming, and if you need to, just pull out a sheet of paper and doodle for some time to get a better look at it.

What is a bit of an issue, however, is that the unit circle your high-school math teacher taught you is the same one your calculator uses, and it is also the same as the one your computer's CPU knows. But, your computer's screen has a different teacher and is a bit confused. So, you'll need to remember that your screen's y-axis is upside down. It is unfortunate, but we all have to live with it. You have to take this into consideration when throwing those pictures on the screen and calculating where up and down are when entering the fun world of computer graphics.

We're looping through all the images from back to front, so when we've placed an image on the screen, we have to make sure that it appears in front of the others which were placed before it by calling the PictureBox's method BrintToFront(). It is like you are dropping pictures on the table. The ones you dropped first wind up being covered by the ones that followed until they're all on the table. This gives you the illusion of depth. The smaller pictures are at the back, and the bigger ones are in front.

Tada, already better than pre-renaissance and you haven't gotten your fingers full of paint.

History

  • Update: 2009 08 13
    • I didn't know, at the time, how to have mouseMove/mouseClick events for a bunch of dynamically generated pictureBoxes and now that I know how to do this no longer need to use a problematic expedient like a troublesome 'transparentLabel' to interact with. So, I deleted the references to this now useless label in the HTML and have attached the edited article along with the two compressed files which appear in the article so that it can be updated
  • Update: 2009 11 15
    • Updated source code and demo

License

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

About the Author

Christ Kennedy
CEO unemployable
Canada Canada
Member
Christ Kennedy, published his fourth novel "Cleats of the Counter Revolution" in the summer of 2010. He grew up in the suburbs of Montreal and is a bilingual Quebecois with a bachelor’s degree in computer engineering from McGill University and is currently walking across ontario plotting a new novel, far away from any computer.

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.
Search this forum  
    Spacing  Noise  Layout  Per page   
Generalthere's bettermemberChrist Kennedy18 Jul '10 - 17:34 
I rewrote this program in a different article Elliptical Rotating Picture Tray and that one is much much better than this one.
my code is perfect until i don't find a bug...

GeneralMovement on an Eclipse ShapememberZahabbiya14 Dec '09 - 2:10 
Hi
I want to know how the current code needs to be modified so that the picture tray moves in an eclipse manner. Its ok if the entire eclipse is not shown..
I also want the picture tray to move in the horizontal manner..Currently the movement is shown on the vertical axis..
 
Zah
GeneralRe: Movement on an Eclipse ShapememberChrist Kennedy14 Dec '09 - 6:31 
thanks for your interest and comments
the way the code is written now changing it to display a pic on the right or left is a bit of trouble as all the testing for mouse movements and pic positioning is done relative to the horizontal axis (left/right) and centers the image at the bottom. its basically hard-wired that way.
you'll notice in the repositionTray() function
 
1. pictures are evenly distributed around an imaginary circle and then in handle_SizeChange() the angle at which the tray is positioned is added to these (radial coordinate)
 
2. then they are positioned as they will appear on the form (cartesian coordinate)
(you'd position them on an ellipse here using something other than sin/cos)
 
3. the cartesian coordinates are then used to calculate which image will appear at the front and which will appear at the back, this is done by placing the images nearest the top of the form in the back (first on the screen) and the pictures nearest the bottom of the form at the front(last on the screen and on top of everybody else).
 
so if you want to do an ellipse you'll have to calculate their positions using an elliptical equation rather than the sin/cos
 
then to put the main picture on the right you'll have to reorder the sequence (step #3) such that the images furthest to the left are at the back(positioned on the screen first) and the images on the right are at the front (put onto the screen last and on top of every other picture).
 
your question has encouraged me to work on this project further and make the circular rotation a special case of an ellipse, that is, make the x-radius & y-radius variable, as well as making the front location of this ellipse variable so that the main image can be displayed not only on any side but at any angle. (i'm busy with other projects for the moment but that's on the back-burner)
 
thanks for your question,
enjoy your project
Christ Kennedy
GeneralRe: Movement on an Eclipse ShapememberChrist Kennedy31 Jan '10 - 5:57 
i got around to it and here it is : Elliptical Rotating Picture Tray.
 
Christ Kennedy
GeneralInteresting !memberBillWoodruff17 Nov '09 - 19:46 
Very creative !
 
I believe you can make your creativity and insights here available to more CP users by making your demo application be able to open multiple files ... which is trivially simple.
 
The first thing I did, to make it more useful to me, was to enable an OpenFileDialog that opened multiple files (isn't that going to be the modal use ?). Second I put a TrackBar on the Form that hosted the PicTray to control the speed of rotation.
 
You can re-use the PictureBox : no need to create a new one with each Image you add, and you don't need to pass it by reference, either.
 
fyi : your code will compile and run in Visual Studio 2010 beta 2 against FrameWork 4.0.
 
The only glitch I have found is that if you position the mouse just on the right edge of a picture the rotation can lock-up where it is obviously cycling between rotating counter-clockwise and clockwise : a kind of "visual stutter" if you will Smile | :) Note that I can reliably repeat this whether bolMouseMoveOverAutoSelect is set to 'true or 'false. And, by the way, I don't know what bolMouseMoveOverAutoSelect is really supposed to do : it doesn't seem to have any effect when I set it to true that I can observe.
 
If you re-organize the code into a more coherent form from its current "spaghetti-ness," I'd vote this #5, but please accept a #4, for now, as a compliment.
 
I look forward to studying your code, and I think it would be very cool if a mouse-click on a picture effectively rotated all the pictures so the clicked picture was in front, and then stopped the rotation until the next mouse click.
 
best, Bill
 
"Many : not conversant with mathematical studies, imagine that because it [the Analytical Engine] is to give results in numerical notation, its processes must consequently be arithmetical, numerical, rather than algebraical and analytical. This is an error. The engine can arrange and combine numerical quantities as if they were letters or any other general symbols; and it fact it might bring out its results in algebraical notation, were provisions made accordingly." Ada, Countess Lovelace, 1844

GeneralRe: Interesting !memberChrist Kennedy18 Nov '09 - 6:19 
thanks for your vote!
I really didn't spend more than four minutes writing the application demo for this project but I very much appreciate your suggestions and made the changes on my PC so that when i get around to documenting it properly and uploading that version of this project your ideas will certainly be considered more closely.
 
as for that 'visual stutter' you make note of and the apparent 'non-effect' of bolMouseMoveOverAutoSelect, I probably didn't explain that variable well enough in the article. When this boolean is set the picture over which the mouse moves most recently is the 'selected picture' and the picTray then rotates either clockwise or counter-clockwise to bring this 'selected' picture to the front. this will explain why sometimes you can place the mouse in the way of the photos and they will constantly rotate as each new image slides beneath the mouse it becomes the selected picture and proceeds to the front bringing the next picture under the cursor, and so on. but if you place the mouse at certain spots the picture moving under the mouse is sometimes on the right of center and sometimes on the left creating this 'visual stutter' effect you noticed.
 
this setting
bolCarousel = true;
will make the tray rotate constantly regardless of picture selection.
 
while this one
bolClickPicBool = true;
is the same as bolMouseMoveOverAutoSelect in the sense that whatever picture is selected will move forward but the mode of selection is 'click' not 'mousemove' or 'mouseenter'.
 
I hope that answers your questions.
Christ Kennedy
Generalno more 'transparent label'memberChrist Kennedy13 Aug '09 - 13:27 
in this update I got rid of the transparent label( which was placed in front of everything and interacted with the user through mouse events then used the e.x & e.Y locations to calculate which pic box the user meant) when I figured out how the user could select from an array of picture boxes and have that selection easily handled by the event handler.
 
There's only one event handler for all the dynamically instantiated pictureboxes and so when the event handler kicks in it needs to know the index of the picture box in the picturebox_array so I created a new class that inherits pictureBox and has an integer called index.
 
this 'Index' variable is where each element of an array holds its own index and so, long story short, when the event handler kicks in the 'object' is turned into my new classPicBox and that has its own index which I use to handle the event.
 

public struct udtPicBoxTrayElement
{
  public classPicBox picBox;
  public udtTrayLoc trayLoc;
  public double dblXOverYRatio;
}
 
public class classPicBox : System.Windows.Forms.PictureBox
{
  public int index;
}
 
int getSelectedImage(System.Windows.Forms.MouseEventArgs e)
{// no longer called
}
 
void picBox_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e)
{
  classPicBox pb = (classPicBox)sender;
  if (bolMouseMoveOverAutoSelect)
    {
    int intTempSelected = pb.index; // this used to call getSelectedImage();
    if (intTempSelected >= 0 & intTempSelected != intSelected)
      {
      intSelected = intTempSelected;
      handle_newSelection();
      }
    }
}

Answerno more jerkingmemberChrist Kennedy7 Apr '09 - 4:04 
I added an extra condition to a line in this event handler. Now, if the mouse is on top of the front pic (e.g. intSelected == intTempSelected) it won't reposition the tray.
 
void lblTransparentLabel_MouseMove(object sender, MouseEventArgs e)
  {
    if (bolMouseMoveOverAutoSelect)
     {
       int intTempSelected = getSelectedImage(e);
       if (intTempSelected >= 0 && intSelected != intTempSelected )
         {
           intSelected = intTempSelected;
           handle_newSelection();
         }
     }
  }

Generalpictures jerking permanentlymemberDamir16 Apr '09 - 20:40 
Hi Christ,
 
first I will say, very good work!
 
When I go with my Cursor about the first picture (the front picture), all pictures jerking permanently approximate 3px to both sides. I think, it is a problem with the code which calculated the correct middle from the window.
 
Damir
AnswerupdatememberChrist Kennedy5 Apr '09 - 12:37 
I added a new zip file for those of you who may have had some trouble compiling and running the code. Because the picTray requires the picViewer DLL you have to compile the picViewer first, then include its DLL into picTray and compile it before you can include both DLLs into the test code and run it. I can see how that can be a problem if its not spelled out for you.
hope this helps.
Christ
GeneralRe: updatememberSeraph_summer6 Apr '09 - 9:47 
thanks, now it works!
 
I did not understand well at beginning. it is better for you to give one example (project example), it is more direct.
 
your work looks good. I hope I can modify it and use it to do sth for explore the photos.
Generalexample...memberChrist Kennedy5 Apr '09 - 7:35 
I'm not sure what it is that doesn't work for you. Did you load and compile the two zipped files into DLLs? If you did then the example I give at the top should work for you. Make sure you include the two DLLs in your project before running the formTestCLibPicTray code.
If you're still having difficulties, please explain what you mean by 'one complete example'.
Christ
Questioncould you please give one complete example which can directly run?memberSeraph_summer5 Apr '09 - 6:57 
it seems I can not directly run your project, could you please give one complete example?
 
like most people do.
 
thanks.

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

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130523.1 | Last Updated 17 Nov 2009
Article Copyright 2009 by Christ Kennedy
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid