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

使用 API 可滚动和缩放的图片浏览器类

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (20投票s)

2007 年 2 月 6 日

CPOL

13分钟阅读

viewsIcon

109658

downloadIcon

1747

这是一个可以滚动和缩放图片的简单类。

Screenshot - KPicView1.jpg

引言

这个类有一些很棒的功能。它可以查看图片,缩放和平移。是的,它能做到,而且做得非常出色:-)。

  • 它使用API函数,所以速度很快。
  • 它同时使用两个鼠标按钮进行滚动和缩放。
  • 当您使用左键拖动时,它会进行正常滚动(拖动的起始点将始终位于鼠标光标下方)。
  • 当您使用右键拖动时,它会进行加速滚动(适合大图片或图片已缩放时)。
  • 缩放是依赖于鼠标位置的,所以它会缩放到您点击的点。
  • 如果当前图像尺寸小于宿主控件,则图像将始终位于宿主控件的中央。
  • 当您在滚动时达到图像边界,然后向另一个方向移动鼠标时,图片不会立即重新开始滚动,直到鼠标越过停止滚动的点。换句话说,您开始滚动的位置将始终位于鼠标光标下方。

这篇长文章可能会给人留下代码冗长而复杂的印象,但实际上,就其功能而言,代码非常简单且小巧。也许在VS编辑器中查看时,它会显得更紧凑且易于理解。

背景

在我讨论这些函数如何工作之前,我将先讨论用于图形API的API函数,这些函数与滚动和缩放无关。

处理API有时会让人头疼,但处理图形API肯定会让人头疼,尽管如此,其结果还是相当惊人的。

我使用的API函数是 'CreateCompatibleDc'、'SelectObject'、'DeleteDC'、'BitBlt'、'StretchBlt'。

我们利用了所谓的设备上下文(DC),您可以将其想象成一个绘制或从中获取绘制的地方。

由于 'BitBlt' 或 'StretchBlt' 用于将图像数据从一个DC传输到另一个DC,因此它们需要一些参数:

  1. 目标DC句柄(HDC),用于标识我们想要粘贴到的DC。
  2. 目标矩形定义了要在目标DC中粘贴复制的数据的位置。
  3. 源DC句柄(HDC),用于标识我们想要从中复制的DC。
  4. 源矩形是源DC中将被复制的区域。
  5. 一个控制传输图像数据外观的参数(对于我们的用途,此参数固定为 'SRCCOPY',值为 '13369376')。

您必须了解DC和HDC之间的区别。基本上,DC是内存中的一个位置,而HDC是一个标识该位置的唯一值,所以每个DC都有一个HDC。

对于目标DC,我们已经拥有它,它是 'Form'、'PictureBox'、'Label' 或我们想要在其上绘制的任何控件的DC。剩下的就是获取它的HDC,现在我们就有了目标HDC。

对于目标矩形,我们获取该 'Form' 或 'PictureBox' 的边界,由一个具有起始点(x,y)和区域(宽度,高度)的矩形定义。

对于源DC,嗯,我们还没有源DC,所以我们使用API函数 'CreateCompatibleDC' 创建一个空的DC,它返回其HDC。

我们现在可以从源HDC到目标HDC使用 'BitBlt',但源DC是空的,里面是黑色的,所以在'Form'上我们看到的全是黑色。

首先,我们必须在源DC中放置一个图像。为此,我们使用API函数 'SelectObject(sourceHDC,HandleBitmap)',它的意思是将句柄为 'HandleBitmap' 的图像放入HDC为 'sourceHDC' 的DC中。

指向位图的位图句柄就像DC的HDC一样。我们通过调用 'Bitmap.GetHbitmap()' 方法来获取位图句柄,该方法返回 'Bitmap' 句柄。

最后,对于源矩形,我们将使用放入源DC的图像的尺寸。

现在,我们才可以进行 'StretchBlt' 或 'BitBlt'。

这个示例代码说明了整个过程。将此代码放入一个 'button_click' 处理程序中,并提供您的图片路径,图片就会显示在主窗体上。但首先,不要忘记声明这些API函数。您可以在附件文件中找到它们。它们占用了大量空间,所以我没有放在这里。

'first of all we load our image from a file into a Bitmap type
Dim srcImage As New Bitmap("MY Image Path")

'we get destination HDC and put it into a variable of type IntPtr
Dim Graph As Graphics = Me.CreateGraphics
Dim desHdc As IntPtr = Graph.GetHdc()

'For source HDC three steps

'create empty DC , get its HDC and put it into a variable of type IntPtr
Dim srcHdc As IntPtr = CreateCompatibleDC(IntPtr.Zero)

'get the handle for our source bitmap and put it into a variable of type IntPtr
Dim hBitmapSrc As IntPtr = srcImage.GetHbitmap()

'put the source Image into the source DC
SelectObject(srcHdc, hBitmapSrc)

'now the source DC is loaded with our image and we have its HDC
'we have destination HDC , we can BitBlt right away
BitBlt(desHdc, 0, 0,  srcImage.Width,  srcImage.Height, srcHdc, 0, 0, 13369376)

代码相当简单,我们可以使用 'StretchBlt' 代替 'BitBlt' 而无需修改代码,但请注意,'StretchBlt' 将源矩形和目标矩形作为参数,而 'BitBlt' 只获取目标矩形。将这段代码从记忆中写出来一次将有助于您可视化其操作。

您也可以使用 'Graphics.DrawImage' 方法来实现相同的结果,但速度差异很大,尤其是在滚动时。我曾为一位朋友制作这个控件,当我知道他打算在其上查看一个27MB、9000*3000像素的.jpg图像时,我决定使用API。

Using the Code

现在回到我们的类...

我们的类操作很简单。我使用一个矩形('MRec')来存储当前将复制到宿主控件的图像部分,一个矩形('BRec')来存储当前复制部分要放置在宿主控件中的位置,以及一个缩放因子 'ZFactor',它告诉是否应在复制到宿主控件之前缩放('MRec')。

这个类有一个主要函数 'DrawPic' 和一些事件处理程序 'MouseDown'、'MouseMove'、'MouseUp'、'Host_Paint' 和 'Host_Resize'。每个事件处理程序(当相应事件触发时)都会对矩形('MRec')或('ZFactor')进行一些更改,然后调用 'DrawPic'。下图解释了我所说的内容。

Screenshot - Mrec.jpg

如果宿主控件被绘制或重设大小,'MRec' 的位置或大小不会发生变化,并且会立即调用 'DrawPic',该函数会将未更改的当前 'MRec' 绘制到宿主控件中。

我使用 'MouseDown'、'MouseUp' 和 'MouseMove' 来检测用户当前是否正在拖动或单击,以及使用的是哪个鼠标按钮。

如果用户正在拖动,则矩形 'MRec' 的X和Y值会被更改,换句话说,拖动会改变 'MRec' 的位置,但不会改变其大小,并且会持续调用 'DrawPic' 函数。

如果用户正在单击,则缩放因子('ZFactor')会根据用户单击的鼠标按钮向上或向下改变,调用 'DrawPic' 并将单击的当前X、Y位置传递给它。

我将讨论三件事:'MouseMove' 事件处理程序,'MouseUp' 事件处理程序和 'DrawPic' 函数,其余的很容易弄清楚。

  1. 'Host_MouseMove' 事件处理程序
    Private Sub Host_MouseMove(ByVal sender As Object, _
    	ByVal e As System.Windows.Forms.MouseEventArgs)
            If IsNothing(srcBitmap) Then Exit Sub
    
            If DownPress = True Then
                Host.Cursor = Cursors.NoMove2D
    
                'accelerated scrolling when right click drag ----------------
                If e.Button = MouseButtons.Right Then
                    CP.X = (P.X - e.X) * (srcBitmap.Width / 2000)
                    CP.Y = (P.Y - e.Y) * (srcBitmap.Height / 2000)
                End If
    
                Mrec.X = ((P.X - e.X) / Zfactor) + Mrec.X + CP.X
                Mrec.Y = ((P.Y - e.Y) / Zfactor) + Mrec.Y + CP.Y
                DrawPic(0, 0)
                If Xout = False Then
                    P.X = e.X
                End If
                If Yout = False Then
                    P.Y = e.Y
                End If
    
            End If
    
    End Sub

    首先,它检查 'srcBitmap' 中是否有图像,如果没有,则 'Sub' 将退出,什么也不会发生。'DownPress' 是布尔类型,在 'MouseDown' 事件触发时设置为 'true',在 'MouseUp' 事件触发时设置为 'false'。之后是右键拖动的加速部分,这种加速取决于图像的大小。

    现在到真正部分,'MRec' 的X和Y值根据鼠标当前位置和上次触发 'MouseMove' 事件时的位置进行增加或减少。为此,我们使用了类型为 'Point' 的('P'),它保存当前鼠标位置,以便在下一次 'MouseMove' 事件触发时使用,我们在 'MouseDown' 事件处理程序中初始化 'P.X' 和 'P.Y'。

    在加速的情况下,将一个常量 'CP.X' 或 'CP.Y' 添加到 'MRec' 的X和Y值中。然后调用 'DrawPic' 函数,其参数设置为 '0 ' DrawPic(0,0) '。现在我们将 '(P.X =e.X)' 和 '(P.Y=e.Y)' 设置为,以便在下一次 'MouseMove' 事件发生时知道移动了多少。

  2. 'Host_MouseUp' 事件处理程序
    Private Sub Host_MouseUp(ByVal sender As Object, _
    	ByVal e As System.Windows.Forms.MouseEventArgs)
            If IsNothing(srcBitmap) Then Exit Sub
    
            DownPress = False
            Host.Cursor = Cursors.Arrow
    
            If CS.X = e.X And CS.Y = e.Y Then
                If e.Button = MouseButtons.Left Then
                    If Zfactor > MaxZ Then Exit Sub
                    oldZfactor = Zfactor
                    Zfactor = Zfactor * 1.3
                    DrawPic(e.X, e.Y)
                ElseIf e.Button = MouseButtons.Right Then
                    If Zfactor < MinZ Then Exit Sub
                    oldZfactor = Zfactor
                    Zfactor = Zfactor / 1.3
                    DrawPic(e.X, e.Y)
                End If
                RaiseEvent ZoomChanged(Zfactor)
            End If
    End Sub

    如果释放的鼠标按钮是左键,它会将当前缩放因子('ZFactor')乘以1.3(放大),然后调用 'DrawPic' 传递鼠标的当前X和Y坐标。如果释放的是右键,它会将当前缩放因子除以1.3(缩小),然后像上面一样调用 'DrawPic'。但在更改当前缩放因子('ZFactor')之前,它会将其复制到 'oldZFactor' 中,供 'DrawPic' 稍后使用。

  3. 'DrawPic' 函数是这个类的主要函数。它基本上完成了所有工作。这个函数分为四个部分,如下所示:
    1. 它检查变量是否已声明,如果未声明,则声明并初始化它们(这只需执行一次)。
      Private Function DrawPic(ByVal ZoomX As Single, _
      	ByVal ZoomY As Single) As Boolean
          If IsNothing(srcBitmap) Then Exit Function
      
          If srcHDC.Equals(IntPtr.Zero) Then
              srcHDC = CreateCompatibleDC(IntPtr.Zero)
              HBitmapSrc = srcBitmap.GetHbitmap()
              SelectObject(srcHDC, HBitmapSrc)
          End If
      
          If desHDC.Equals(IntPtr.Zero) Then
              If IsNothing(Gr) Then
                  Gr = Host.CreateGraphics
              End If
              desHDC = Gr.GetHdc()
              SetStretchBltMode(desHDC, 3)
          End If

      首先,我们声明函数('DrawPic'),它接受两个变量('ZoomX','ZoomY'),这些变量仅在发生缩放操作(单击)时传递给函数,否则它们为零。

      我们检查我们的源位图('srcBitmap')类型为 'Bitmap' 当前是否包含图片,如果没有,我们退出此函数,不做任何事情。

      然后我们检查是否存在源DC(通过检查 'if srcHDC=0'),如果不存在,我们通过调用 'CreateCompatibleDC' 创建一个,它现在只是一个空的DC,里面什么都没有。之后,我们必须在其中放入一张图片。为了能够放入一张图片,我们必须首先使用 'srcBitmap.GetHbitmap()' 获取该图片的句柄,然后使用其HDC调用 'Selectobject (srcHDC,HBitmapSrc)' 将图片放入源DC。

      之后,我们检查是否拥有目标DC,如果没有,我们通过先从宿主控件创建一个 'Graphics' 对象(如果尚未创建)然后调用 'Graphics.GetHdc()' 方法来获取宿主控件的DC,该方法返回HDC。

      然后我们调用 'SetStretchBltMode' 函数将我们目标DC的模式设置为3,即 'COLORNOCOLOR'。

      现在,从前面的讨论中,您应该知道我们完成了上述步骤,只获得了两个变量,我们非常重要的两个变量:目标DC句柄('desHDC')和源DC句柄('srcHDC'),它们将在函数稍后由 'StretchBlt' 使用。

    2. 'DrawPic' 函数的这一部分与上一部分完全分开。这是数学计算的部分。
      Xout = False
      Yout = False
      
      If Host.Width > srcBitmap.Width * Zfactor Then
          Mrec.X = 0
          Mrec.Width = srcBitmap.Width
          Brec.X = (Host.Width - srcBitmap.Width * Zfactor) / 2
          Brec.Width = srcBitmap.Width * Zfactor
          
          BitBlt(desHDC, 0, 0, Brec.X, Host.Height, srcHDC, _
      	0, 0, TernaryRasterOperations.BLACKNESS)
          BitBlt(desHDC, Brec.Right, 0, Brec.X, Host.Height, _
      	srcHDC, 0, 0, TernaryRasterOperations.BLACKNESS)
      Else
          Mrec.X = Mrec.X + ((Host.Width / oldZfactor - _
      	Host.Width / Zfactor) / ((Host.Width + 0.001) / ZoomX))
          Mrec.Width = Host.Width / Zfactor
          Brec.X = 0
          Brec.Width = Host.Width
      End If
      
      If Host.Height > srcBitmap.Height * Zfactor Then
          Mrec.Y = 0
          Mrec.Height = srcBitmap.Height
          Brec.Y = (Host.Height - srcBitmap.Height * Zfactor) / 2
          Brec.Height = srcBitmap.Height * Zfactor
          
          BitBlt(desHDC, 0, 0, Host.Width, Brec.Y, srcHDC, 0, _
      	0, TernaryRasterOperations.BLACKNESS)
          BitBlt(desHDC, 0, Brec.Bottom, Host.Width, Brec.Y, _
      	srcHDC, 0, 0, TernaryRasterOperations.BLACKNESS)
      Else
          Mrec.Y = Mrec.Y + ((Host.Height / oldZfactor - _
      	Host.Height / Zfactor) / ((Host.Height + 0.001) / ZoomY))
          Mrec.Height = Host.Height / Zfactor
          Brec.Y = 0
          Brec.Height = Host.Height
      End If
      
      oldZfactor = Zfactor

      您可以看到这部分被平均分成两个 'IF' 语句,第一个 'IF' 语句与 'MRec'、'BRec'、'Host'、'srcBitmap' 的X和Width值有关。下一个 'IF' 语句执行相同操作,但针对 'MRec'、'BRec'、'Host'、'srcBitmap' 的Y和Height。现在我们可以解释一个 'IF' 语句,另一个将是相同的。

      在第一个 'IF' 语句中,它检查宿主控件的宽度是否大于图像乘以缩放因子。它试图确定是否整个图像都将包含在宿主控件中,因为如果是这样,它将能够看到整个图像,如果我们能够看到整个图像,那么 'MRec'(这是从源复制到目标的区域)必须设置为包含整个图像。所以我们设置 'MRec.X=0' 和 'MRec.Width=srcBitmap.Width',对于另一个 'IF' 语句,'MRec.Y=0' 和 'MRec.Width=srcBitmap.Width'。

      'BRec' 是用来做什么的? 程序没有它也能运行(意味着您可以完全从程序中删除它),但当您对图片进行更多缩小时,小图片将不会出现在宿主控件的中央,而是会靠边对齐。所以我们设置 'BRec.X' 和 'BRec.Width' 来选择宿主控件中用于居中显示 'MRec' 的区域。

      这两个 'BitBlt' 函数用于在图片小于宿主控件时在图片周围绘制黑色区域。您可以删除它们,看看会发生什么。

      现在,所有这些讨论都将在图像或缩放后的图像小于宿主控件的情况下发生,但如果不是这样呢?

      在这种情况下,我们设置 'BRec.X=0' 和 'BRec.Width=Host.Width',这是合乎逻辑的,意味着显示的图像将占据整个宿主控件区域。

      但是 'MRec.X' 和 'MRec.Width' 呢?嗯… 'MRec.Width' 将等于 'Host.Width' / 'ZFactor',这是有意义的,因为 'MRec' 会被 'ZFactor' 缩放。对于 'MRec.X',您应该注意到,如果 'oldZFactor' 等于 'ZFactor',这意味着不会发生缩放,也就是说,此函数是由 'MouseMove'、'Host_Paint' 或 'Host_Rsize' 调用,而不是由 'MouseUp' 调用。如果 'oldZFactor=ZFactor',那么右边的项将等于 'MRec.X',因为 '(Host.Width / oldZfactor - Host.Width / ZFactor)' 将等于零。如果 'oldZFactor' 不等于 'ZFactor',则 'MRec' 将被定位以接近点击的缩放点。

    3. 这部分检查 'MRec' 是否在图像边界内。如果不在,它将重新定位它,使其在图像边界内。
      If Mrec.X < 0 Then
          Xout = True
          Mrec.X = 0
      End If
      
      If Mrec.Y < 0 Then
          Yout = True
          Mrec.Y = 0
      End If
      
      If Mrec.Right > srcBitmap.Width Then
          Xout = True
          Mrec.X = (srcBitmap.Width - Mrec.Width)
      End If
      
      If Mrec.Bottom > srcBitmap.Height Then
          Yout = True
          Mrec.Y = (srcBitmap.Height - Mrec.Height)
      End If

      这部分很简单。它检查 'MRec' 的X或Y值是否小于零,如果是,则将其设置为零,然后检查 'MRec' 的右侧和底部值是否大于图像宽度或高度,如果是,则重新定位 'MRec',使其右侧或底部值等于图像宽度或高度。由于 'MRec.Right' 和 'Mrec.Buttom' 是只读的,我们无法修改它们,所以我们通过简单的减法计算新的X或Y位置。(请注意,这部分仅操作 'MRec' 的位置,而不改变其宽度或高度,不改变其大小)。

    4. 这部分根据 'MRec' 和 'BRec' 的值实际绘制图像到宿主控件中。
          StretchBlt(desHDC, Brec.X, Brec.Y, Brec.Width, Brec.Height, _
                srcHDC, Mrec.X, Mrec.Y, Mrec.Width, Mrec.Height, _
          TernaryRasterOperations.SRCCOPY)
      
          Gr.ReleaseHdc(desHDC)
          desHDC = Nothing
      End Function 'The end of DrawPic function

      正如我们之前所说,'StretchBlt' 函数需要5个参数。'StretchBlt' 函数无法将源矩形或目标矩形作为 'Rectangle' 类型接受,它将(X,Y,Width,Height)分别作为源和目标的独立参数,所以实际上它需要11个参数。

      最后,我们通过调用 'Graphics.ReleaseHdc(HDC)' 方法释放宿主DC,这是一个非常重要的步骤,因为假设您想使用任何 'Graphics' 函数,例如 'Gr.DrwaEllipse',如果您的代码在调用 'ReleaseHdc' 之前,程序会返回错误,您应该在HDC释放后放置您的代码。因此,'desHDC' 在每次 'DrawPic' 调用结束时都会被释放。

关注点

希望您能从中有所收获。通过(在进行必要的更改后),这个类还可以用作图形编辑程序的一部分。

在上面的代码中,我们看到适用于X的值也适用于Y,适用于Width的值也适用于Height,这基本上意味着如果我们生活在一个二维世界里,代码可以减半。 :)

暂时就到这里,请享用。

历史

  • 更新于2007年5月1日:修复了程序内存泄漏,现在完全没有内存泄漏了,太棒了!
© . All rights reserved.