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

图像查看器 UserControl

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (106投票s)

2010 年 4 月 7 日

CPOL

10分钟阅读

viewsIcon

406742

downloadIcon

26915

本文介绍了我编写的一个用户控件。与在窗体上显示图像的 PictureBox 和其他方法不同,这个控件提供了一种完全不同的方法。

ImageViewerUserControl/KpImageViewerV2.png

引言

ImageViewer UserControl是我为了解决我在窗体上显示图像时遇到的一个问题而创建的。我希望能够缩放、旋转我的图像,并且最重要的是,不必为了适应我的窗体而缩小图像。

我四处寻找并找到了诸如将 PictureBox 拖放到面板中之类的解决方案,这可能不错,但也有其问题。通过本文,我想与那些可能面临同样问题的人分享我的工作。

用户控件的属性

AllowDrop bool 一个用于启用或禁用控件上拖放功能的属性。
BackgroundColor Color 一个用于获取或设置图片面板背景颜色的属性。
GifAnimation bool 一个用于启用或禁用 *.gif 文件动画的属性。
GifFPS double 一个用于调整 *.gif 文件动画的每秒帧数的属性。范围在 1 到 30 FPS 之间。
Image Bitmap 一个用于获取或设置控件显示的图像的属性。
ImagePath 字符串 一个用于设置图像的物理路径的属性(C:\Image.jpg)。
MenuColor Color 一个用于调整整个菜单使用的颜色的属性。
MenuPanelColor Color 一个用于仅调整菜单面板使用的颜色的属性。
OpenButton bool 一个用于启用或禁用 UserControl 菜单上“打开”按钮的属性。
NavigationPanelColor Color 一个用于仅调整导航面板使用的颜色的属性。
PanelWidth int 一个返回图片面板宽度的属性。
PanelHeight int 一个返回图片面板高度的属性。
PreviewButton bool 一个用于启用或禁用 UserControl 菜单上“预览”切换按钮的属性。
PreviewPanelColor Color 一个用于仅调整预览面板使用的颜色的属性。
PreviewText 字符串 一个用于编辑预览标签文本的属性。
滚动条 bool 一个用于启用或禁用滚动条的属性。
TextColor Color 一个用于调整所有标签使用的颜色的属性。
NavigationTextColor Color 一个用于调整导航标签使用的颜色的属性。
PreviewTextColor Color 一个用于调整预览标签使用的颜色的属性。
旋转 int 一个用于获取或设置图像旋转角度(0、90、180 或 270 度)的属性。
ShowPreview bool 一个用于启用或禁用预览面板的属性。
缩放 int 一个用于获取缩放百分比的属性。
OriginalSize 大小 一个用于获取图像原始尺寸的属性。
CurrentSize 大小 一个用于获取图像当前尺寸的属性。

用户控件的事件

AfterRotation 图像旋转后触发的事件。

可用属性
旋转 int 一个获取图像旋转角度(0、90、180 或 270 度)的属性。
AfterZoom 图像放大或缩小时触发的事件。

可用属性
缩放 int 一个获取缩放百分比的属性。
InOut KpZoom 一个返回是 ZoomIn 操作还是 ZoomOut 操作的属性。

Using the Code

与任何 UserControl 一样,将其拖到窗体上即可。要将控件添加到工具箱,请执行以下步骤:

  • 步骤 1:右键单击工具箱,然后单击“选择项...”
  • 步骤 2:在“.NET Framework 组件”中,单击“浏览”按钮。
  • 步骤 3:浏览到解压后的文件夹,然后选择“KP-ImageViewerV2.dll”。
  • 步骤 4:确保选中 KpImageViewer,然后单击“确定”。

ImageViewer 具有内置的“打开图像”按钮。但是,如果这不是您想要的,则可以通过编程方式设置图像,并通过将 OpenButton 属性设置为 false 来禁用“打开”按钮。

private void Form1_Load(object sender, EventArgs e)
{
    kpImageViewer.OpenButton = false;
    kpImageViewer.Image = new Bitmap(@"C:\chuckwallpaper.jpg");
}

此外,还有 3 种旋转功能可供使用。非常直接。

private void Form1_Load(object sender, EventArgs e)
{
    kpImageViewer.Rotate90(); // Rotates the Image 90 degrees clockwise
    kpImageViewer.Rotate180(); // Rotates the Image 180 degrees clockwise
    kpImageViewer.Rotate270(); // Rotates the Image 270 degrees clockwise
}

用户控件代码

KpImageViewer 类继承自 System.Windows.Forms.UserControl 类。该类使用 2 个单独的类和一个额外的 UserControlDrawEngineDrawObject 是使用的类,而 UserControl 是一个 DoubleBufferedPanel

public class PanelDoubleBuffered : System.Windows.Forms.Panel
{
    public PanelDoubleBuffered()
    {
        this.DoubleBuffered = true;
        this.UpdateStyles();
    }
}

    public partial class KpImageViewer : UserControl
    {
        private KP_DrawEngine drawEngine;
        private KP_DrawObject drawing;

        ...
    }

DrawEngine 负责在内存中存储一个与面板精确大小相同的位图。它将用于在内存中渲染图像并将其绘制到面板上。DrawEngine 在调整大小时会重新创建内存位图,以使高度和宽度与面板相等。

public void InitControl()
{
    drawEngine.CreateDoubleBuffer(pbFull.CreateGraphics(), pbFull.Width, pbFull.Height);
}

private void KP_ImageViewerV2_Resize(object sender, EventArgs e)
{
    InitControl();
    drawing.AvoidOutOfScreen();
    UpdatePanels(true);
}

DrawObject 包含 Viewer 的所有实际功能。它负责在内存中存储原始图像、缩放、旋转、拖动和跳转到原点(在预览面板上单击的位置)。这些功能由 KpImageViewer 类中触发的事件调用。例如,请看鼠标功能的片段。这些功能负责图像的拖动和选择。

private void pbFull_MouseDown(object sender, MouseEventArgs e)
{
   if (e.Button == MouseButtons.Left)
   {
      // Left Shift or Right Shift pressed? Or is select mode one?
      if (this.IsKeyPressed(0xA0) || this.IsKeyPressed(0xA1) || selectMode == true)
      {
         // Fancy cursor
         pbFull.Cursor = Cursors.Cross;

         shiftSelecting = true;

         // Initial selection
         ptSelectionStart.X = e.X;
         ptSelectionStart.Y = e.Y;

         // No selection end
         ptSelectionEnd.X = -1;
         ptSelectionEnd.Y = -1;
      }
      else
      {
         // Start dragging
         drawing.BeginDrag(new Point(e.X, e.Y));

         // Fancy cursor
         if (grabCursor != null)
         {
            pbFull.Cursor = grabCursor;
         }
      }
   }
}

private void pbFull_MouseUp(object sender, MouseEventArgs e)
{
   // Am i dragging or selecting?
   if (shiftSelecting == true)
   {
      // Calculate my selection rectangle
      Rectangle rect = CalculateReversibleRectangle(ptSelectionStart, ptSelectionEnd);

      // Clear the selection rectangle
      ptSelectionEnd.X = -1;
      ptSelectionEnd.Y = -1;
      ptSelectionStart.X = -1;
      ptSelectionStart.Y = -1;

      // Stop selecting
      shiftSelecting = false;

      // Position of the panel to the screen
      Point ptPbFull = PointToScreen(pbFull.Location);

      // Zoom to my selection
      drawing.ZoomToSelection(rect, ptPbFull);

      // Refresh my screen & update my preview panel
      pbFull.Refresh();
      UpdatePanels(true);
   }
   else
   {
      // Stop dragging and update my panels
      drawing.EndDrag();
      UpdatePanels(true);

      // Fancy cursor
      if (dragCursor != null)
      {
         pbFull.Cursor = dragCursor;
      }
   }
}

private void pbFull_MouseMove(object sender, MouseEventArgs e)
{
   // Am I dragging or selecting?
   if (shiftSelecting == true)
   {
      // Keep selecting
      ptSelectionEnd.X = e.X;
      ptSelectionEnd.Y = e.Y;

      Rectangle pbFullRect = new Rectangle(0, 0, pbFull.Width - 1, pbFull.Height - 1);

      // Am I still selecting within my panel?
      if (pbFullRect.Contains(new Point(e.X, e.Y)))
      {
            // If so, draw my Rubber Band Rectangle!
         Rectangle rect = CalculateReversibleRectangle(ptSelectionStart, ptSelectionEnd);
         DrawReversibleRectangle(rect);
      }
   }
   else
   {
      // Keep dragging
      drawing.Drag(new Point(e.X, e.Y));
      if (drawing.IsDragging)
      {
         UpdatePanels(false);
      }
      else
      {
         // I'm not dragging OR selecting
         // Make sure if left or right shift is pressed to change cursor

         if (this.IsKeyPressed(0xA0) || this.IsKeyPressed(0xA1) || selectMode == true)
         {
            // Fancy Cursor
            if (pbFull.Cursor != Cursors.Cross)
            {
               pbFull.Cursor = Cursors.Cross;
            }
         }
         else
         {
            // Fancy Cursor
            if (pbFull.Cursor != dragCursor)
            {
               pbFull.Cursor = dragCursor;
            }
         }
      }
   }
}

函数: AvoidOutOfScreen() 用于防止图像浮出面板外部。它被编程为确保图像永远不会离开左上角(X: 0, Y: 0)。

我在这里遇到的主要问题是,一旦您拖动图像,就不希望 X 或 Y 坐标高于零。这本身不是问题,但当您查看 boundingBox.LeftboundingBox.Top 时,它就会出现。这些值将是负数,而 boundingBox.WidthboundingBox.Height 将是正数。将这些值相加会导致值不正确,并使图像拖动不正确。

我需要一个函数来确保 X、Y 坐标永远不高于零,并且永远不低于(图像宽度 - PanelWidth) - ((图像宽度 - PanelWidth) * 2)。

对于一个 480x320 的查看器和一个 1024x768 的图像,您将得到以下公式:

(1024 - 480 - ((1024 - 480) * 2)) = -544 

这意味着最小 X 值将是 -544,以避免右侧出现浮动图像。

这是另一个可视化的例子,说明它是如何工作的。此图像的尺寸为 512x384。(请注意,这只是一个矩形,实际图像并未显示在屏幕外)

您可以在此处看到最小 X 值将是 -234。如果低于该值,您将在面板的右侧看到空白区域。

AvoidOutOfScreen() technique

public void AvoidOutOfScreen()
{
   try
   {
      if (boundingRect.X >= 0)
      {
         boundingRect.X = 0;
      }
      else if ((boundingRect.X <= (boundingRect.Width - panelWidth) -
				((boundingRect.Width - panelWidth) * 2)))
      {
         if ((boundingRect.Width - panelWidth) -
		((boundingRect.Width - panelWidth) * 2) <= 0)
         {
            boundingRect.X = (boundingRect.Width - panelWidth) -
			((boundingRect.Width - panelWidth) * 2);
         }
         else
         {
            boundingRect.X = 0;
         }
      }

      if (boundingRect.Y >= 0)
      {
         boundingRect.Y = 0;
      }
      else if ((boundingRect.Y <= (boundingRect.Height - panelHeight) -
				((boundingRect.Height - panelHeight) * 2)))
      {
         if((boundingRect.Height - panelHeight) -
			((boundingRect.Height - panelHeight) * 2) <= 0)
         {
            boundingRect.Y = (boundingRect.Height - panelHeight) -
			((boundingRect.Height - panelHeight) * 2);
         }
         else
         {
            boundingRect.Y = 0;
         }
      }
   }
   catch (Exception ex)
   {
      System.Windows.Forms.MessageBox.Show("ImageViewer error: " + ex.ToString());
   }
}

函数: ZoomToSelection() 是 1.2 版本中的一项新功能,用于放大选定区域。在这里,我们计算出适合我们传入的选择的缩放位置和缩放量。我们还将一个 Point 变量传递到面板的 X,Y 坐标 PointToScreen()

public void ZoomToSelection(Rectangle selection, Point ptPbFull)
{
   int x = (selection.X - ptPbFull.X);
   int y = (selection.Y - ptPbFull.Y);
   int width = selection.Width;
   int height = selection.Height;

   // So, where did my selection start on the entire picture?
   int selectedX = (int)((double)(((double)boundingRect.X -
		((double)boundingRect.X * 2)) + (double)x) / zoom);
   int selectedY = (int)((double)(((double)boundingRect.Y -
		((double)boundingRect.Y * 2)) + (double)y) / zoom);
   int selectedWidth = width;
   int selectedHeight = height;

   // The selection width on the scale of the Original size!
   if (zoom < 1.0 || zoom > 1.0)
   {
      selectedWidth = Convert.ToInt32((double)width / zoom);
      selectedHeight = Convert.ToInt32((double)height / zoom);
   }

   // What is the highest possible zoomrate?
   double zoomX = ((double)panelWidth / (double)selectedWidth);
   double zoomY = ((double)panelHeight / (double)selectedHeight);

   double newZoom = Math.Min(zoomX, zoomY);

   // Avoid Int32 crashes!
   if (newZoom * 100 < Int32.MaxValue && newZoom * 100 > Int32.MinValue)
   {
      SetZoom(newZoom);

      selectedWidth = (int)((double)selectedWidth * newZoom);
      selectedHeight = (int)((double)selectedHeight * newZoom);

      // Center the selected area
      int offsetX = 0;
      int offsetY = 0;
      if (selectedWidth < panelWidth)
      {
         offsetX = (panelWidth - selectedWidth) / 2;
      }
      if (selectedHeight < panelHeight)
      {
         offsetY = (panelHeight - selectedHeight) / 2;
      }

      boundingRect.X = (int)((int)((double)selectedX * newZoom) -
		((int)((double)selectedX * newZoom) * 2)) + offsetX;
      boundingRect.Y = (int)((int)((double)selectedY * newZoom) -
		((int)((double)selectedY * newZoom) * 2)) + offsetY;

      AvoidOutOfScreen();
   }
}

功能: 拖放!(1.2 版本新增) 现在已支持!当 AllowDrop 设置为 true 时,面板将接受文件拖放到其上。为此,我像下面这样重载了 UserControl 上现有的 AllowDrop 属性。

public override bool AllowDrop
{
   get
   {
      return base.AllowDrop;
   }
   set
   {
      this.pbFull.AllowDrop = value;
      base.AllowDrop = value;
   }
}

没什么特别的,我只是需要确保面板具有与 UserControl 本身相同的 AllowDrop 值。至于实际的拖放代码:

private void pbFull_DragDrop(object sender, DragEventArgs e)
{
   try
   {
      // Get The file(s) you dragged into an array.
      // (We'll just pick the first image anyway)
      string[] FileList = (string[])e.Data.GetData(DataFormats.FileDrop, false);

      Image newBmp = null;

      for (int f = 0; f < FileList.Length; f++)
      {
         // Make sure the file exists!
         if (System.IO.File.Exists(FileList[f]))
         {
            string ext = (System.IO.Path.GetExtension(FileList[f])).ToLower();

            // Checking the extensions to be Image formats
            if (ext == ".jpg" || ext == ".jpeg" || ext == ".gif" ||
		ext == ".wmf" || ext == ".emf" || ext == ".bmp" ||
		ext == ".png" || ext == ".tif" || ext == ".tiff")
            {
               try
               {
                  // Try to load it into a bitmap
                  newBmp = Bitmap.FromFile(FileList[f]);
                  this.Image = (Bitmap)newBmp;

                  // If succeeded stop the loop
                  break;
               }
               catch
               {
                  // Not an image?
               }
            }
         }
      }
   }
   catch (Exception ex)
   {
      System.Windows.Forms.MessageBox.Show("ImageViewer error: " + ex.ToString());
   }
}

private void pbFull_DragEnter(object sender, DragEventArgs e)
{
   try
   {
      if (e.Data.GetDataPresent(DataFormats.FileDrop))
      {
         // Drop the file
         e.Effect = DragDropEffects.Copy;
      }
      else
      {
         // I'm not going to accept this unknown format!
         e.Effect = DragDropEffects.None;
      }
   }
   catch (Exception ex)
   {
      System.Windows.Forms.MessageBox.Show("ImageViewer error: " + ex.ToString());
   }
}

关注点

这是我第二次尝试创建具有这些功能的 ImageViewer。我的第一次尝试是让一个 PictureBox 拖到一个 Panel 上。最初这似乎效果不错,但无法在较大的图像上提供一个真正有效的缩放系统(堆大小问题)。这个查看器可以无限缩放而无需使用额外的内存。

已知问题

  • 如果控件没有焦点,滚动缩放将不起作用。(单击控件或图像足以重新获得焦点)。
  • 打开动画 *.gif 文件时,预览图像不会动画(这是故意的!)。
  • 不支持带有 JPEG 压缩的多页 tiff 图像。Microsoft GDI+ 未对此问题提供任何解决方案。

版本历史

版本 1.5.1: (2011 年 9 月 12 日)

  • 修复了 FitToScreen 函数返回不正确结果的问题
    • 从项目中移除了所有 static 代码(导致了此问题)
    • 添加了两个额外的属性 PanelWidthPanelHeight

版本 1.5.0: (2011 年 9 月 1 日)

  • 重新设计了 GIF 动画
    • 移除了 ImageAnimator 并实现了一个自定义锁定系统
    • 现在可以通过 GifFPS 属性指定帧速率
  • 修复了某些 *.gif 图像旋转 180 度后不再动画的问题
  • 修复了将多页 tiff 图像拖放到控件上时发生的崩溃
  • 新功能: 完全支持滚动条

版本 1.4.0: (2011 年 8 月 15 日)

  • 重新上传了演示项目。演示中现在又可以进行拖放了。

版本 1.4.0: (2011 年 8 月 2 日) 完整的更改列表

  • 实现了对动画 *.gif 文件支持

版本 1.3.5: (2010 年 6 月 21 日) 完整的更改列表

  • 进一步修复了多页 TIFF 旋转问题

版本 1.3.4: (2010 年 6 月 19 日) 完整的更改列表

  • 修复了多页 TIFF 旋转
  • 修复了通过 UNC 路径打开图像
  • 修复了多页菜单的位置
  • 修复了打开非图像格式(例如 *.txt)时双重 TryCatch

版本 1.3.3: (2010 年 5 月 6 日) 完整的更改列表

  • 添加了 EMF/WMF 图像支持(感谢 wsmwlh!)
  • 拖放现在也接受 EMF/WMF
  • 对多页检查进行了 TryCatch(在 WMF 文件上崩溃)
  • 修复了打开多页 Tiff 图像后,在打开非图像格式(例如 *.txt)时发生的 NullReference

版本 1.3.2: (2010 年 5 月 5 日) 完整的更改列表

  • 修复了导航面板位置的一些进一步问题

版本 1.3.1: (2010 年 5 月 5 日) 完整的更改列表

  • 修复了隐藏预览面板时导航面板位置不当的问题(感谢 wsmwlh 的提醒!)

版本 1.3: (2010 年 5 月 5 日) 完整的更改列表

  • 添加了多页 TIFF 支持(请求)
  • 为多页导航添加了其他属性
  • 清理了解决方案(移除了重复图像及其引用)

版本 1.2: (2010 年 4 月 26 日) 由于资源图像过多,重新上传了演示项目和源代码文件(尽管有效,但不太好看!)

版本 1.2: (2010 年 4 月 23 日) 完整的更改列表

  • 添加了拖放功能(请求)
  • 增加了放大选中区域的可能性(选择缩放)
  • 添加了一个新按钮和快捷方式(Shift + 鼠标单击)以使用选择缩放
  • 添加了单页 TIF 支持
  • 修复了打开只读图像时的 ReadOnly bug
  • 为大量代码添加了注释,使其更清晰
  • 修复了几个小 bug

版本 1.1.1: (2010 年 4 月 14 日) 完整的更改列表

  • 修复了隐藏预览面板后绘制时的微小 bug
  • 修复了预览面板错误折叠的 bug

版本 1.1: (2010 年 4 月 14 日) 完整的更改列表

  • 添加了额外的 Color 属性以进行单独的颜色更改
    • 背景颜色(图片面板)
    • 预览标签颜色
    • 菜单的单独颜色可能性
  • 修复了 AvoidOutOfScreen() 处理宽图像时的 bug
  • 优化了预览图像的渲染
  • 现在可以在预览面板内进行拖动
  • 现在可以启用或禁用预览面板
    • 用户控制预览面板的按钮
    • 可以在代码中强制设置(参见属性:ShowPreview & PreviewButton
  • 现在可以通过在 ComboBox 中输入特定数字并按 Enter 来进行缩放
  • 添加了一些漂亮的“手”和“拖动”光标

版本 1.0: (2010 年 4 月 7 日) 首次公开发布版本

结论

创建这个控件对我来说是一次非常有趣的经历,我希望很多人都能在他们的项目中找到它是一个有用的控件。虽然我认为该控件运行良好,但我认为仍有很大的改进空间。源代码也已提供,供那些想对其进行修改的人使用。我请求的是,如果您发现任何 bug 或对代码进行任何改进,也请在此处提交,以便大家都能享受您的工作!感谢您的阅读,也许下次文章再见。

致谢

我想感谢 NT Almond (Norm .net) 的文章 使用 GDI+ 和 C# 进行无闪烁绘图UserControl 使用此技术在其面板上实现无闪烁绘图。

© . All rights reserved.