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

扩展图像查看器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.58/5 (10投票s)

2009年3月4日

CPOL

5分钟阅读

viewsIcon

47665

downloadIcon

3886

一个具有增强功能的图片控件。

img2.jpg

目录

介绍

不久前,我参与了一个为某政府土地管理部门开发数字档案系统的团队。基本上,该系统试图做的是组织和数字化管理部门档案中的纸质文件,并提供档案的数字副本。该系统旨在解决文档流转速度、并发访问等许多问题。

该系统带来的好处之一是对文档页面进行详细、全面的访问,用户比在大量文件中翻阅纸质文档更能清晰地查看文档。由于这些文档可以追溯到几十年前,而且其中大部分是手写的,因此这一功能是该系统颇具吸引力的功能之一。.NET 包中包含的 PictureBox 控件非常适合以简单的方式显示单个图像,但我们需要一个增强版的控件,以便更好地预览显示的图像,并且还需要一个可以处理多个图像的控件。

我翻阅了我的个人项目,找到了一个我独自开始的项目,它可以成为这个解决方案的起点。最终在数字档案系统中实现的版本功能更强大、更复杂,但它依赖于我提出的设计。

从最基本的、粗略的 terms 来说,在查看一组图像以进行全面访问时,最基本的需求是:

  1. 平移
  2. 缩放
  3. 从一幅图像导航到另一幅图像
  4. 旋转

我决定设计一个控件,它可以更详细地轻松执行这些任务。此控件的实现涉及一些小的计算,这些计算基本上仅限于坐标转换和缩放。让我来澄清一下涉及的转换和坐标系。

img1.jpg

坐标转换是从一个坐标系到另一个,或者反之。

img3.jpg

ImageViewer 组件是程序集中的主要组件,因为所有计算和命令都在其中实现。它几乎包含了查看单个图像所需的所有命令,除了旋转,所有这些我都将其简化为显示中心点的平移和缩放值的缩放的函数。例如,在抓取和平移图像时,我们将该操作描述为中心点和缩放值的函数。

平移

  • 缩放值保持不变
  • 显示中心点平移了
      • x 坐标的变化和
      • y 坐标的变化

以下是所有操作的描述方式:

private void pic_MouseMove(object sender, MouseEventArgs e)
{
    //when a mouse is down and moving 
    //panning and region selection are posibilities
    switch (previewMode)
    {
        case PreviewMode.REGIONSELECTION:
            if (e.Button==MouseButtons.Left)
            {
                //display a rectangular region to the selected region 
                //to notify the user what he is selecting
                int w = Math.Abs(tempCenter.X - e.X), h = Math.Abs(tempCenter.Y - e.Y);
                if (w > 1 || h > 1)
                {
                    this.Refresh();
                    Graphics gr = pic.CreateGraphics();
                    gr.DrawString("(" + (tempCenter.X + e.X) / 2 + "," + 
                                 (tempCenter.Y + e.Y) / 2 + ")", this.Font, 
                                 Brushes.Khaki, new PointF((tempCenter.X + e.X) / 2, 
                                 (tempCenter.Y + e.Y) / 2));
                    gr.DrawRectangle(Pens.Red, new Rectangle((tempCenter.X + e.X - w) / 2, 
                                    (tempCenter.Y + e.Y - h) / 2, w, h));
                    gr.Dispose();
                }
            }
            break;
        case PreviewMode.PAN:
            if (e.Button==MouseButtons.Left&&(tempCenter.X != e.X || 
                                                      tempCenter.Y != e.Y))
            {
                displayCenter = new Point(displayCenter.X + 
                    (int)((tempCenter.X- e.X ) / mZoom),
                    displayCenter.Y + (int)((tempCenter.Y-e.Y) / mZoom));
                ZoomImage();
                tempCenter = e.Location;
            }
            break;
        default:
            break;
    }
}

private void pic_MouseUp(object sender, MouseEventArgs e)
{
    //when the mouse is up its when most of the previews are commited
    switch (previewMode)
    {
        case PreviewMode.REGIONSELECTION:
            displayCenter = new Point(displayCenter.X + 
                (int)(((double)(tempCenter.X + e.X - this.Width) / 2) / mZoom),
                displayCenter.Y + (int)(((double)(tempCenter.Y + 
                e.Y - this.Height) / 2) / mZoom));

    double z = mZoom * pic.Width / Math.Abs(tempCenter.X - e.X);
    if (mZoom * pic.Height / Math.Abs(tempCenter.Y - e.Y) < z)
        z = mZoom * pic.Height / Math.Abs(tempCenter.Y - e.Y);
    mZoom = z;
    ZoomImage();
    break;
        case PreviewMode.ZOOMIN:
            //when zoom in the zoom value is simply multiplied by two
            displayCenter=new Point(displayCenter.X+(int)((e.X-this.Width/2)/mZoom),
                displayCenter.Y+(int)((e.Y-this.Height/2)/mZoom));
            mZoom *= 2;
            //redraw the image with the new zoom value
            ZoomImage();
            break;
        case PreviewMode.ZOOMOUT:
            //when zoom out the zoom value is simply divided by two
            displayCenter = new Point(displayCenter.X + 
                (int)((e.X - this.Width / 2) / mZoom), 
                 displayCenter.Y + (int)((e.Y - this.Height / 2) / mZoom));
            mZoom /= 2;
            //redraw the image with the new zoom value
            ZoomImage();
            break;
        default:
            break;
    }
}

如代码所示,每个 case 语句都用于计算显示结果图像所需的两个最重要变量。一旦计算出这些值,如果您调用 ZoomImage() 方法,它将有效地使用新值重绘图像。类图部分看起来像:

img4.jpg

实现

托管图像查看器组件

此组件包含所有必需的命令,并已准备好供用户代码使用,但要使其成为一个非常有用且有效的查看器控件,您需要像在用户控件中一样托管它,并提供事件来触发每个命令。随本文一起,我包含了一个时尚的主机控件,易于使用,如图所示:

img5.jpg

此控件具有工具箱实现的所有基本工具,如图所示。可用的每个命令都通过按钮和下拉列表公开。例如,组件的缩放功能通过下拉列表事件处理程序公开,如下面的示例代码所示:

private void cmbZoom_SelectedIndexChanged(object sender, EventArgs e)
{
    try
    {
        //those values with % value on them they are percent values
        //send this zoom value to the component
        if (cmbZoom.Text.IndexOf('%') != -1)
        {
            img.ZoomImage(double.Parse(cmbZoom.Text.Trim('%')) / 100.0);
        }
        else
        {
            //if its an enumeration then parse the text to get the enumeration
            img.ZoomImage((ZeeImaging.ZoomMode)
              (Enum.Parse(typeof(ZeeImaging.ZoomMode), cmbZoom.Text, true)));
        }
    }
    catch
    {
        cmbZoom.Text = "";
    }
}

为了减少我需要编写的代码量来提取每个缩放模式的枚举,我采用了将字符串解析为其枚举值的方法;否则,它将是一个直接的方法来公开图像查看器组件的 ZoomImage 方法。工具条按钮也是如此,只是我使用了每个按钮的 tag 来存储相应的枚举值,这样我就不必 switch … caseif … else 处理所有情况,也不必处理所有按钮的 Click 事件。如下面的示例代码所示:

private void btn_Click(object sender, EventArgs e)
{
    img.ImagePreviewMode = (PreviewMode)Enum.Parse(typeof(PreviewMode), 
                           ((ToolStripButton)sender).Tag.ToString());
    btnPan.Checked = img.ImagePreviewMode == PreviewMode.PAN;
    btnRegionZoom.Checked = img.ImagePreviewMode == PreviewMode.REGIONSELECTION;
    btnZoomIn.Checked = img.ImagePreviewMode == PreviewMode.ZOOMIN;
    btnZoomOut.Checked = img.ImagePreviewMode == PreviewMode.ZOOMOUT;
    switch (img.ImagePreviewMode)
    {
        case PreviewMode.PAN:
            this.Cursor = Cursors.Hand;
            break;
        case PreviewMode.REGIONSELECTION:
            this.Cursor = Cursors.Cross;
            break;
        default:
            this.Cursor = Cursors.Default;
            break;

    }
}

图像源

在大多数情况下(并非全部),图像的来源是文件系统,系统会尝试显示图像文件。但无论如何,图像查看器组件接受任何图像源,只要该对象实现了 IZImage 接口,其定义如下面的代码所示:

/// <summary>
/// a signiture interface that when implemented by any object
/// enables it to utilize the image viewer component
/// </summary>
public interface IZImage
{
    //gets the number of images that are available for display
    int ImageCount{ get;}
    //gets the current index of the image that is being displayed
    int CurrentIndex { get;}
    //retreives the next available image
    Image GetNextImage();
    //retrieves the previously displayed image
    Image GetPreviousImage();
}

在本文附带的示例代码中,在 sample.csPictureBoxEx.cs 文件中有两个 IZImage 接口的示例实现。在示例实现中,我编写了一个简单的代码来显示目录中的图像。我创建了一个名为“DirectoryImages”的类,其定义如下所示:

public class DirectoryImages:IZImage
{
    //path of the directory
    string m_DirectoryName;
    //number of the image files that are found
    int m_ImageFilesCount;

    int m_CurrentIndex=-1;
    string[] ImageFiles;

    public DirectoryImages(string str)
    {
        m_DirectoryName = str;
        ImageFiles = Directory.GetFiles(str, "*.jpg");
        m_ImageFilesCount = ImageFiles.Length;
    }

    #region IZImage Members

    public int ImageCount
    {
        get { return m_ImageFilesCount; }
    }

    public int CurrentIndex
    {
        get { return m_CurrentIndex; }
    }

    public System.Drawing.Image GetNextImage()
    {
        m_CurrentIndex++;
        if (m_CurrentIndex >= m_ImageFilesCount)
            throw new Exception("No More Images");
        return System.Drawing.Image.FromFile(ImageFiles[m_CurrentIndex]);
    }

    public System.Drawing.Image GetPreviousImage()
    {
        m_CurrentIndex--;
        if (m_CurrentIndex <= 0)
            throw new Exception("No More Images");
        return System.Drawing.Image.FromFile(ImageFiles[m_CurrentIndex]);
    }

    #endregion
}

如代码所示,此对象获取目录路径,并逐个显示该目录中扩展名为“jpg”的图像。要显示对象从任何源获取的任何图像,请实现“IZImage”接口并将对象传递给宿主控件。为了快速使用此图像查看器,请阅读下一节。

快速实现

计算机 OOP 最伟大的成就之一是细节的抽象,以及为了使用一个类而无需知道其实现细节的事实。因此,我提供了一个默认的宿主控件实现,使其行为类似于 PictureBox;此定义可以在 “PictureBoxEx.cs” 文件中找到。

当您阅读代码时,您会发现基本上有两种实现:宿主控件(PictureBoxEx)和图像源类(ImageFile)。

示例应用

我编译的示例应用程序希望能最好地描述使用此增强型图像查看器的强大功能。您可以使用“文件”菜单打开单个图像文件,或通过指定文件夹来浏览文件夹中的图像。

img6.jpg

结论

总之,您可以为该组件构建复杂的宿主控件,而无需担心组件中涉及的计算;例如,可以集成鼠标中键来进行缩放。我希望该组件能对任何希望使用它的人有所帮助。

© . All rights reserved.