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

可滚动、可缩放、可缩放的图片框

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.24/5 (7投票s)

2012年12月10日

CPOL

6分钟阅读

viewsIcon

32283

downloadIcon

2494

"可滚动、可缩放、可缩放的图片框" 的增强版

引言   

此项目基于 Bingzhe Quan 在以下地址的项目“可滚动、可缩放、可缩放的图片框”:https://codeproject.org.cn/Articles/15373/A-scrollable-zoomable-and-scalable-picture-box。我们正在开发一个涉及将图片作为背景缩放的设计软件,并发现原项目非常有用。然而,我们的项目需要一些功能,但这些功能在原项目中不幸缺失。

  1. 图片可以缩放到任意大小,并且 Quan 在改进部分承认了缺失的“更大的放大倍率”功能。这背后的主要驱动力是,在我们的项目中,小图片应该放大到更大的尺寸,以便详细查看图像,或者将相当大的图片缩放到适应窗口,以便一次性查看整个图像。这需要对原始图像进行缩放,并可能进行额外的像素插值。
  2. 原始图像可以进行自定义,这同样需要重新绘制图像。在我们的代码中,我们展示了如何在原始图像周围添加一个简单的灰色边框。如果不是过于复杂,可以以类似的方式实现进一步的图像操作。
  3. 除了通过弹出菜单缩放图像外,像 Google 地图那样使用鼠标滚轮缩放图像也会很好。这是一个小的 UI 增强,但 nevertheless 证明是有用的,因为它还提供了更广泛的缩放选择。

背景

原作者使用 Facade 和 Mediator 设计模式设计了该项目,而这个增强版本认为没有必要更改该设计。因此,我们引用 Quan 的原始文本和图表来解释项目结构,如下所示。

ScalablePictureBoxScalablePictureBox 控件的门面。它也协调 ScalablePictureBoxImpPictureTrackerScalablePictureBoxImp 是可滚动、可缩放、可缩放图片框的核心实现。PictureTracker 是当前图片的跟踪器、滚动器和缩略图查看器。TransparentButtonPictureTracker 中用作关闭按钮的一个小型用户控件。Util 提供了一些控件的辅助函数。” 

本文绝非一篇独立成文的文章。因此,您应该参考 Quan 的文章以获取有关原始项目如何设计和工作的更多信息。

使用代码

主要的代码更改发生在 ScalablePictureBoxImp 类中。我们引入了一个名为 OriginalPicture 的属性来存储原始图片,以便新的缩放图片可以从原始图片生成,而不是基于前一个缩放图片。事实证明,这可以避免因缩小操作可能造成的像素丢失对后续缩放图片的影响,从而提高了缩放图片的显示质量。

// A copy of the original picture 
private Bitmap originalPicture;
public Bitmap OriginalPicture
{
   get { return originalPicture; 
   set
   {
     originalPicture = value;
 
     if (value == null)
       return;
 
     LeftX = 0;
     UpperY = 0;
     RightX = value.Width;
     LowerY = value.Height;
}

为了保留缩放图像尺寸的引用,还使用了另外两个属性 ScaledPictureWidthScaledPictureHeight。此外,还添加了几个属性 LeftXUpperYRightXLowerY 来保留缩放图像在 PictureBox 内的逻辑位置的引用,因为图像不再总是占据整个 PictureBox

// Keep record of the dimensions of original & scaled images for fast reference and calculation
public int ScaledPictureWidth { get; set; }
public int ScaledPictureHeight { get; set; }  
// Keep record of the logic positions of the image corner points for fast reference and calculation
private int leftX;
public int LeftX { get { return leftX; } set { leftX = value; } }
private int upperY;
public int UpperY { get { return upperY; } set { upperY = value; } }
private int rightX;
public int RightX { get { return rightX; } set { rightX = value; } }
private int lowerY;
public int LowerY { get { return lowerY; } set { lowerY = value; } }

在这里,我们将缩放百分比定义为缩放图像与原始图像的比率,即原始图像的缩放百分比为 100%,放大图像的缩放百分比大于 100%,而缩小图像的缩放百分比小于 100%。

为了同时使用鼠标滚轮和弹出菜单来缩放图像,并避免在没有缩放变化时进行不必要的重绘,我们记录了之前的缩放百分比(在 PreviousScalePercent 中),以便与新指定的缩放百分比(在 CurrentScalePercent 中)进行比较。由于原始缩放百分比为 100%,第一次通过鼠标滚轮缩放操作会使原始图像大小增加 1%,即新的缩放百分比将为 100%*1.01 = 101%。同样,第一次通过鼠标滚轮缩小操作会使原始图像大小减少 1%,即新的缩放记录将为 100%/1.01 = 99.01%。任何进一步的鼠标滚轮滚动将分别增加或减少之前的缩放百分比 1%。

虽然这种通过鼠标滚轮滚动的预设缩放通常工作得很好,但对于接近 100% 的缩放百分比来说,效果并不那么出色。原因很简单:如果当前的缩放百分比是 400%,那么进一步的放大操作将使其增加到 400% * 1.01 = 404%,而两个缩放百分比之间的差异将是 4%。但是,如果当前的缩放百分比是 100%,进一步的放大操作只会产生 1% 的差异。如果原始图像尺寸很小,这不太明显。

// Previous scale percentage for the picture box
public int PreviousScalePercent { get; set; }
 
// Scale percentage of picture box in zoom mode
private int currentScalePercent = Common.ORIGINALSCALEPERCENT;
// Scale percentage for the picture box
public int CurrentScalePercent
{
  get { return currentScalePercent; }
  set
  {
    // No image or the scale remains the same, no need to redraw
    if (PictureBox.Image == null || PreviousScalePercent == value)
      return;
 
    // Calculate the minimum and maximum scale percentages allowed by predefined values for the
    // width and height respectively to make sure neither of them exceeds the preset upper and lower scale limits
    int minScalePercent = (int)(100 * Math.Max((float)Common.PICTUREWIDTHMIN / (float)OriginalPicture.Width,
      (float)Common.PICTUREHEIGHTMIN / (float)OriginalPicture.Height));
    int maxScalePercent = (int)(100 * Math.Min((float)Common.PICTUREWIDTHMAX / (float)OriginalPicture.Width,
      (float)Common.PICTUREHEIGHTMAX / (float)OriginalPicture.Height));
 
    // Set the previous scale percent and the current one
    PreviousScalePercent = CurrentScalePercent;
    currentScalePercent = Math.Max(Math.Min(value, maxScalePercent), minScalePercent);
 
    // Set the parent control scale percent to pass the value to the PictureTracker
    scalablePictureBoxParent.ScalePercent = CurrentScalePercent;
 
    // Calculate the scaled picture dimensions
    ScaledPictureWidth = (int)(OriginalPicture.Width * (float)currentScalePercent / (float)Common.ORIGINALSCALEPERCENT);
    ScaledPictureHeight = (int)(OriginalPicture.Height * (float)currentScalePercent / (float)Common.ORIGINALSCALEPERCENT);
 
    // Redraw scaled picture 
    ReDrawPicture();
  }
}

为了简单解决这个问题,我们在缩放百分比变化之间引入了 10 像素的阈值。因此,在放大/缩小图片时,新图片通过增加 1% 或 10 像素(以较大者为准)的尺寸来使其与前一个图片明显不同。

// Zoom in the picture
private void ZoomInPicture()
{
 // Make sure the zoom in ratio is noticeable but also within reasonable range
 PreviousScalePercent = CurrentScalePercent;
 CurrentScalePercent = Math.Max(PreviousScalePercent + 10, (int)(Common.ZOOMINRATIO * CurrentScalePercent));
}
 
// Zoom out the picture
private void ZoomOutPicture()
{
 // Make sure the zoom out ratio is noticeable but also within reasonable range
 PreviousScalePercent = CurrentScalePercent;
 CurrentScalePercent = Math.Min(PreviousScalePercent - 10, (int)(Common.ZOOMOUTRATIO * CurrentScalePercent));
}

此外,我们还添加了对父 ScalablePictureBox 控件的引用,以便 ScalablePictureBoxImp 中的当前缩放百分比 CurrentScalePercent 可以传递回 ScalablePictureBox,从而同步 PictureTracker 中的变量 ScalePercent

// The referenece to the parent ScalablePictureBox
public ScalablePictureBox scalablePictureBoxParent { get; set; } 
...
// Set the parent control scale percent to pass the value to the PictureTracker
scalablePictureBoxParent.ScalePercent = CurrentScalePercent;

引入了一个布尔标志 mouseWheelEventHandled,以确保 pictureBox_MouseWheel() 的函数体仅在鼠标滚轮按下和鼠标滚轮再次抬起之间执行一次。

// The flag to stop the pictureBox_MouseWheel event handler being triggered more than once during one scrolling
private bool mouseWheelEventHandled = true;
 
public void pictureBox_MouseWheel(object sender, System.Windows.Forms.MouseEventArgs e)
{
  // Check the flag. Early out if the flag is true
  if (!mouseWheelEventHandled)
    return;
 
  ZoomRate = e.Delta / 120;
  // Set the flag to false to mark the mouse wheel event has not been handled
  mouseWheelEventHandled = false;
}
 
public void pictureBox_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
{
  // Mouse button up means the wheel scrolling has finished - Set the flag to false
  mouseWheelEventHandled = true;
}

我们引入的最后一个变量是 ZoomList,它用于填充弹出菜单,作为通过鼠标滚轮滚动来缩放图片的另一种方式。

public class ZoomItem
{
  public int ZoomRate { get; set; }
  public string ZoomName { get; set; } 

  ...
}
public List<ZoomItem> ZoomList { get; set; }
/// <summary>
/// Initialize ZoomList
/// </summary>
private void InitialiseZoomList()
{
  if (ZoomList == null)
    ZoomList = new List<ZoomItem>();
  else
    ZoomList.Clear();
 
  /// Fit width zoom rate is unknow at this stage since the image is NOT loaded yet.
  ZoomList.Add(new ZoomItem(Common.FITWIDTHMENUITEMNAME));
  /// Fit height zoom rate is unknow at this stage since the image is NOT loaded yet.
  ZoomList.Add(new ZoomItem(Common.FITHEIGHTMENUITEMNAME));
  // Add a separator
  ZoomList.Add(new ZoomItem("-"));
 
  ZoomList.Add(new ZoomItem(Common.MINSCALEPERCENT));
 
  for (int scale = 25; scale <= 150; scale += 25)
  {
    ZoomList.Add(new ZoomItem(scale));
  }
 
  for (int scale = 200; scale <= Common.MAXSCALEPERCENT; scale += 200)
  {
    ZoomList.Add(new ZoomItem(scale));
  }
}

关注点

调整原始图像  

此版本的一个主要改进是,原始图像可以被修改,而不仅仅是被缩小,这是在 ReDrawPicture() 函数中完成的。

首先,我们创建一个名为 pictureWithFrame 的新 Bitmap。其想法是将其用作画布,并分几步在上面绘制修改后的图像。目前,我们所做的是将缩放后的图像定位在 pictureWithFrame 的中心,并在其周围添加四条条带作为图片框。

为了添加背景条带,我们在 FillBitMapRegion() 中使用了一些不安全的代码,以便用特定颜色填充区域内的像素。Bitmap.LockBits()Bitmap.UnlockBits() 用于将 Bitmap 锁定到系统内存,以便可以根据其在内存中的地址快速设置 Bitmap 像素。同时,我们定义并使用了一个新的结构 Overlay 来将 32 位颜色填充到 4 个字节中。填充几个矩形区域作为图片框没什么值得大惊小怪的,但重要的是将 Bitmap 变量用作画布的想法。

/// <summary>
/// Generic function to fill BitMap region on screen or in PictureBox control
/// </summary>
/// <param name="bmap"></param>
/// <param name="colour"></param>
/// <param name="leftX"></param>
/// <param name="upperY"></param>
/// <param name="rightX"></param>
/// <param name="lowerY"></param>
/// <returns></returns>
unsafe private bool FillBitMapRegion(ref Bitmap bmap, 
       Color colour, int leftX, int upperY, int rightX, int lowerY)
{
  leftX--; upperY--; rightX--; lowerY--;
  BitmapData bmd = bmap.LockBits(new Rectangle(leftX, 
      upperY, rightX - leftX + 1, lowerY - upperY + 1),
      ImageLockMode.ReadOnly, bmap.PixelFormat);
  int PixelSize = 4;
  Overlay overlay = new Overlay();
  for (int y = 0; y < bmd.Height; y++)
  {
    byte* row = (byte*)bmd.Scan0 + (y * bmd.Stride);
    for (int x = 0; x < bmd.Width; x++)
    {
      overlay.u32 = (uint)colour.ToArgb();
      row[x * PixelSize] = overlay.u8_0;
      row[x * PixelSize + 1] = overlay.u8_1;
      row[x * PixelSize + 2] = overlay.u8_2;
      row[x * PixelSize + 3] = overlay.u8_3;
    }
  }
  bmap.UnlockBits(bmd);

  return true;
}

第二步是用缩放后的图像填充 pictureWithFrame 的中心。我们使用 System.Drawing.Graphics.DrawImage()OriginalPicture 绘制到 pictureWithFrame 中的指定区域,而不是绘制前一个缩放后的图像。正如我们之前提到的,这是为了提高图像质量。同时,将 Graphics.InterpolationMode 设置为 InterpolationMode.HighQualityBilinear,以便在放大 OriginalPicture 时在像素之间进行插值。

最后一步是将这个调整过的图像设置为 pictureBox.Image 并刷新弹出菜单。

/// <summary>
/// Reraw the changed image in memory and assign its value to Picture
/// </summary>
/// <returns></returns>
private bool ReDrawPicture()
{
  Bitmap pictureWithFrame;
 
  // Get the four corner point coordinates of the scaled image
  switch (GetZoomedPicturePosition(ref leftX, ref upperY, ref rightX, ref lowerY))
  {
    case Common.SUCCESSBUTNOTDONE:
      return true;
    case Common.GENERALERROR:
      return false;
    case Common.GENERALSUCCESS:
      break;
    default:
      throw new NotImplementedException();
  }
 
  // The background frames need to be filled every time the foreground is zoomed
  if (!FillBackground(out pictureWithFrame, leftX, upperY, rightX, lowerY))
    return false;
 
  // Draw the zoomed image
  System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(pictureWithFrame);
  g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBilinear;
  g.DrawImage(OriginalPicture, leftX, upperY, rightX, lowerY);
 
  // Set the picture dimensions
  PictureBox.Width = ScaledPictureWidth;
  PictureBox.Height = ScaledPictureHeight;
 
  // Set the picture
  Picture = pictureWithFrame;
 
  return true;
}

我们包含了一个简单的演示项目来展示如何使用此控件。如果您在使用控件本身或演示过程中遇到任何问题,请告知我们。

© . All rights reserved.