65.9K
CodeProject 正在变化。 阅读更多。
Home

椭圆旋转图片托盘和编辑器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (18投票s)

2010年1月31日

CPOL

12分钟阅读

viewsIcon

40969

downloadIcon

1527

一个可以在灵活的尺寸和角度的椭圆周围旋转的图片托盘,带有一个 C# 编辑器。

capture_novels.PNG

引言

图片托盘是一组虚拟的轨道,你的图片会沿着这些轨道在屏幕上旋转。这个项目最初的版本,旋转图片托盘,是我大约一年前编写并发布的,它仍然运行良好,但有一些局限性,这个版本解决了这些问题。原版最重要的问题是它难以使用。用它编程简直不够简单。此外,最明显的区别是,旧版本将图片的运动限制在圆形路径上,虽然这仍然是一种炫目的查看照片的方式,但大多数程序员会希望能够调整圆形的大小和高/宽比例,使其变成任何尺寸可变的椭圆,并且还能够将该椭圆朝任何方向旋转,然后选择最适合托盘前方的方式。我的意思是,旧版本旋转的图片在屏幕底部很大,那里视觉效果会使它们看起来比顶部变小的其他图片更近。这个版本允许你做到这一切并且能够将前方指向任何你喜欢的地方。

如何使用代码

使用此类非常简单。首先,你必须在你的 IDE 中添加对它的引用。然后,创建一个图片托盘的实例,并将其添加到你的窗体控件中,它就可以使用了。这是一个示例代码,可以轻松完成此操作:

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);
            }

        }
    }
}

如果你一直在使用旧版本,你可能会注意到,你不再传递 PictureBox,而只传递图片到托盘中。 That's because it's the main difference between how 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.

说到这个...

椭圆到底是什么?

我们都知道什么是圆,但什么是椭圆?但你看,问题不应该是“什么是椭圆”,因为实际上,是圆比较奇怪,因为圆只是一种稀有的椭圆,它的宽半径等于它的高半径,或者用数学术语来说,有两个相同的焦点。但我不会用太多几何知识来烦你,而是让我们来看看这个项目的机制,因为在你开始将它编程到你的应用程序中之前,你需要了解一些信息。也就是说,只有当你想要稍微深入了解一下的时候,因为这个类本身已经准备好使用了,无需任何努力。

如果你还在阅读这篇文章,那很可能是因为你渴望更伟大的成就。

那么,开始吧!

椭圆就像一个在垂直或水平方向上被压扁的圆。它仍然关于任一中心线对称,但宽度或高度不相等。这个程序不仅允许你根据喜好设定椭圆的宽度,高或矮,胖或瘦,甚至可以把它变成一个圆。要做到这一点,你有两个基本的椭圆调整参数:EllipseRadius_WidthEllipseRadius_Height。它们就像它们听起来那样,所以如果你拿出计算器进行一点三角学运算,你就可以看到下面这些代码行,它们将图像放置在以原点为中心的椭圆上,作为定位相同图像在输出位图上某个位置的中间步骤。

// position on unturned ellipse about origin
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);

你可以在这里看到上面提到的公共属性名称的私有版本,这里称为 'dblEllipse_Width' 和 'dblEllipse_Height',它们分别乘以 Cos 和 Sin,以获得该图像在以原点为中心的椭圆上的位置的笛卡尔(x, y)坐标。

这在刷新屏幕的一个时钟周期内作为中间步骤完成。整个托盘的工作方式与旧版本非常相似,但我们仍然可以回顾一下。首先,所有图像都存储在一个 rotatingTray_PicBox 类型的数组中,如下所示:

public class rotatingTray_PicBox
{
    public double angleOnTray;
    public double rotatedAngle;         // angle ON tray + angle OF tray
    public double dblAngularDistanceFromFront; 
    public short shoDirToFront;         // Sign or factor to direct rotation
    public double aspectRatioYoverX;
    public string caption;              
    public bool FlipV;                  // original is vertically inverted
    public bool FlipH;                  // original is horizontally inverted
    public Point pt;                    // last location on screen
    public Size sz;                     // last size on screen
    public int Index;                   // index in original array
    public Image Image; 
}

这些字段中的许多都可以通过查看它们的名称轻松解释,例如 captionptszimage,但你可能想知道 shoDirToFrontdblAngularDistanceFromFront 是什么意思。短变量“Direction to Front”(到前方的方向)在每个时钟周期都会被计算和重置,它告诉程序,如果用户点击该图像并希望以最短路径将其带到前方,托盘需要朝哪个方向旋转。另一个神秘的变量是 double 类型的变量,称为“Angular Distance From the Front”(距离前方的角度距离)。正如我在本文开头提到的,上一个版本的项目的一个主要缺点是缺乏灵活性,因为它被硬编码为面向屏幕底部。由于我创建这个新版本的第一个目标是允许程序员选择托盘的哪一侧是前方,因此,必须使用另一种方法来计算哪个图像最接近前方,以及接近多少(因为最后一个版本仍然在计算像素!)。

最接近底部的图像就是最接近前方的图像。但在本例中,我们处理的是一个可变的“前方”,并且不能再与屏幕的底部边缘对齐了。现在你玩的是大人物的游戏了!所以,要做到这一点,我们将所有角度保持在 FrontAngleFrontAnglePlusTwoPi 之间 [FrontAngle, FrontAngle + 2π)

/// <summary>
/// sets the reference angle to a value between [FrontAngle, FrontAngle + 2pi)
/// </summary>
/// <param name="angle" />reference to a double variable</param>
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);
}

我应该简要概述一下刷新时发生的事情

  1. 创建一个大小与 Tray[] 数组相同的指针数组。

  2. 该算法在初步评估过程中会循环遍历每个条目,在此期间,新数组中的每个指针都链接到原始数组,并且每个条目都会计算出它到前方的新角度距离。这个值可以介于 [0, 2π) 之间,所以我们减去 2π,现在它介于 [-π, π) 之间。

    这一步如下所示:

    Tray_Reordered = new rotatingTray_PicBox[Tray.Length];
    
    /// set img rotated angles 
    ///   - calculate their angular distance from the front
    ///   - insert each into temp pointer array ready to be reordered back to front
    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);
        // calculate angular distance from front  e.g. = [0, pi)
        Tray[intCounter].dblAngularDistanceFromFront = Math.Abs(dblDistance);
        Tray_Reordered[intCounter] = Tray[intCounter];
        Tray[intCounter].Index = intCounter;
    }
  3. 接下来,我们使用快速排序算法对指针数组进行重新排序,从最远到最近,这样当我们扫描指针数组时,我们可以先绘制最远的图像,然后它们就可以被更靠近前方的图像覆盖。
  4. 下一步是根据它们在椭圆上的位置,在屏幕上绘制图像的过程。这需要几个步骤:
    1. 在快速排序之后,临时指针数组现在有一个列表,从托盘最远的后方(索引零)到最近的前方(数组末尾)。因此,我们可以按顺序扫描整个指针数组,首先根据它们与前方的距离计算它们的大小。
    2. 然后,我们使用 sin/cos 函数和上面显示的 radiusWidthradiuHeight 变量来找到如果我们将图像放置在一个以原点为中心的未旋转椭圆上时它会是什么位置。
    3. 然后,要将此椭圆旋转 dblEllipseAngle 角,我们需要将这些笛卡尔坐标转换为极坐标,旋转极坐标,然后将结果旋转后的位置重新转换为它们新的笛卡尔位置,然后才能在屏幕上绘制它们。将未旋转的椭圆定位在原点使半径计算更简单。为了在屏幕上放置图像,我们使用旋转后的极坐标,然后从椭圆中心移开那个距离,并在那里绘制图像。
    4. 你会认为那就是全部了吧,对吧?但是,这个程序还有一个额外功能,就是能够将整个输出水平或垂直翻转。这只是在将图像放入屏幕之前调用 .NET 原生位图函数 RotateFlip() 的一个简单过程。
    5. 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;
      }

但如果我们这样做,我们将看到我们最初意图的镜像。这在某些应用程序中可能没问题,但如果你处理的是文本,水平翻转将很难阅读,而垂直翻转几乎总是一个问题。所以,首先,我们必须对我们粘贴到位图上的图像进行相同的操作,然后翻转整个输出并重新将其纠正。我们可以复制存储在 Tray[] 数组中的原始图像,但这会减慢速度!所以,相反,我们维护两个布尔变量,一个用于垂直翻转,一个用于水平翻转(你可能已经在上面的 rotatingTray_PicBox 类中注意到了)。这些布尔变量跟踪当前对原始图像执行了哪些翻转操作,因此,如果我们只需要垂直翻转,并且看到图像已经通过测试布尔变量 flipV 进行了反转,那么我们就直接显示它。如果它不是我们想要的,那么我们再次翻转它并重置布尔变量以反映这个变化。

看看下面的代码...

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;
}

所以,在这里,你可以看到我们需要将操作分离成 V 和 H 翻转。第一个 if 语句通过测试是否需要翻转且图像尚未翻转,或者是否需要一个常规的未翻转图像且内存中的原始图像已翻转来处理垂直翻转;在这两种情况下,它都会翻转图像并切换布尔值。

但是如何利用所有这些灵活性呢?

如果这个程序的最后一个版本使用起来像一场噩梦,那么这个版本就更糟糕了。太糟糕了。简直是太糟糕了(没有图形编辑器的话!)。我可以坐在桌边,摆弄变量, fiddling with it for hours and still not get what I was looking for.

必须做些什么...

Pic_Tray_mechanics.png

上面的图片可能在你开始使用编辑器时有所帮助(然后想象一下没有编辑器的情况下进行操作!)。首先,你需要知道 EllipseAngle 仅在屏幕上可见。在内部,椭圆是未旋转的,因此所有计算都相对于 xy 平面进行,然后显示在屏幕上时进行旋转。这仅在你试图确定前方在哪里时很重要,因为前方角度相对于椭圆角度,就好像椭圆角度为零并沿正 x 轴运行一样。然后,你需要记住,你的三角学书中的第一象限不适用于计算机屏幕,这是一个问题,因为很久以前有人决定将原点放在屏幕的左上角,这使得你的 sin() 函数不是一半时间,而是所有时间都是错误的!所以,你要么在使用它之前在其前面加上一个负号,要么就习惯于屏幕上显示的这个角度与纸上的角度不同。

editor_Main.PNG

幸运的是,图形界面确实非常直观。滚动条可以让你控制许多参数,包括托盘的尺寸,图像宽度或高度的最小值/最大值,以及通过按下 ENTER 键激活的文本框以手动输入值。在“name”字段中写入名称(并按 ENTER!)将有助于你稍后使用 C#Code 选项。至于上面显示的其余字段和模式,大多数都是不言自明的。也许,“Front Angle User Set Mode”(前方角度用户设置模式)需要提及一下,因为它是一个奇怪的功能。你可以允许用户决定你的椭圆的哪一端是前方。通过一个未旋转的拥挤的圆来做到这一点,可以产生一个很酷的效果,类似于鼠标在物体上移动时浏览旋转托盘,或者你可以强制用户点击它才能将托盘的前方转向那个方向。

editor_Ellipse.PNG

在这张图片中,你可以选择让你的托盘在八个方向中的任何一个方向上自行调整大小,或者你可以手动调整设置来自己旋转和调整它。这里的优势相当重要,因为让它自行调整大小可以让你安心,你知道窗体的大小会波动,但如果你知道尺寸是固定的,那么你可以根据需要进行微调。

考虑到所有涉及的变量和正在发挥作用的机制,你可以想象一下,通过在代码和执行之间来回切换,直到你终于在托盘方面取得一些进展,要达到你所设想的确切效果是多么困难。这并非适用于所有配置,但图形界面在这里仍然是一个巨大的优势。

现在,想象一下盯着下面的照片,它们在你眼前悠闲地掠过,就像一场漂亮的游行,然后你想在你的应用程序中包含类似的东西,但又不想费力地记笔记并在放回原处之前把它们全部写在纸上。编辑器的用途是什么?

当椭圆达到预期状态时,你按下一个按钮,生成代码的代码就会自动出现在你的指尖。而且!只需快速点击并粘贴,这段新生成的代码就会整齐地写在你的 IDE 中。

你不相信吗?看看这个。

picTray_Capture.PNG

完全正确!完全正确。

但是,设置所有参数的过程很繁琐!幸运的是,你的编辑器的第三个标签页控制着 C# 代码,这里有一个快速点击和粘贴就可以为你写好所有代码。

edito_C__code.PNG

而且,在 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

你仍然需要自己添加图像,但除此之外,你就完成了!

© . All rights reserved.