我试图编写一个自定义的可滚动c#控件,该控件将缩放到图像上的特定点。我面临的问题是,启用双重缓冲后,图像似乎会向左上角倾斜,然后正确缩放到单击鼠标的位置。这似乎仅在设置AutoScrollPosition时发生。我验证了在我的OnPaint方法中不会发生这种情况。这似乎是我无法追踪的一些内部行为。有人解决了这个问题吗?

这是一些示例代码,演示了我要完成的工作。当图像相当大时,该问题似乎只会对用户明显显现。

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
using System.Drawing;

namespace Zoom
{
    public class PointZoom : ScrollableControl
    {
        #region Private Data
        private float _zoom = 1.0f;
        private PointF _origin = PointF.Empty;
        private Image _image;
        private Matrix _transform = new Matrix();
        #endregion

        public PointZoom() {
            SetStyle(ControlStyles.UserPaint, true);
            SetStyle(ControlStyles.AllPaintingInWmPaint, true);
            SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
            this.AutoScroll = true;
            UpdateScroll();
        }

        public Image Image {
            get {
                return _image;
            }
            set {
                _image = value;
                _origin = PointF.Empty;
                _zoom = 1.0F;
                UpdateScroll();
                Invalidate();
            }
        }

        protected override void OnPaintBackground(PaintEventArgs e) {
            // don't allow the background to be painted
        }

        protected override void OnPaint(PaintEventArgs e) {

            Graphics g = e.Graphics;

            ClearBackground(g);

            float dx = -_origin.X;
            float dy = -_origin.Y;

            _transform = new Matrix(_zoom, 0, 0, _zoom, dx, dy);
            g.Transform = _transform;

            DrawImage(g);
        }

        private void ClearBackground(Graphics g) {
            g.Clear(SystemColors.Window);
        }

        protected override void OnScroll(ScrollEventArgs se) {
            if (se.ScrollOrientation == ScrollOrientation.HorizontalScroll) {
                _origin.X += se.NewValue - se.OldValue;
            }
            else {
                _origin.Y += se.NewValue - se.OldValue;
            }
            Invalidate();
            base.OnScroll(se);
        }



        protected override void OnMouseClick(MouseEventArgs e) {
            ZoomToPoint(e.Location);
            Invalidate();
        }

        private void UpdateScroll() {

            if (_image != null) {

                Size scrollSize = new Size(
                    (int)Math.Round(_image.Width * _zoom),
                    (int)Math.Round(_image.Height * _zoom));
                Point position = new Point(
                    (int)Math.Round(_origin.X),
                    (int)Math.Round(_origin.Y));

                this.AutoScrollPosition = position;
                this.AutoScrollMinSize = scrollSize;
            }
            else {
                this.AutoScrollMargin = this.Size;
            }

        }

        private void ZoomToPoint(Point viewPoint) {

            PointF modelPoint = ToModelPoint(viewPoint);

            // Increase the zoom
            _zoom *= 1.25F;

            // calculate the new origin
            _origin.X = (modelPoint.X * _zoom) - viewPoint.X;
            _origin.Y = (modelPoint.Y * _zoom) - viewPoint.Y;

            UpdateScroll();
        }

        private PointF ToModelPoint(Point viewPoint) {
            PointF modelPoint = new PointF();

            modelPoint.X = (_origin.X + viewPoint.X) / _zoom;
            modelPoint.Y = (_origin.Y + viewPoint.Y) / _zoom;

            return modelPoint;
        }

        private void DrawImage(Graphics g) {
            if (null != _image) {
                // set the transparency color for the image
                ImageAttributes attr = new ImageAttributes();
                attr.SetColorKey(Color.White, Color.White);
                Rectangle destRect = new Rectangle(0, 0, _image.Width, _image.Height);
                g.DrawImage(_image, destRect, 0, 0, _image.Width, _image.Height, GraphicsUnit.Pixel, attr);
            }
        }

        protected override void Dispose(bool disposing) {
            if (disposing) {
                if (null != _image) {
                    _image.Dispose();
                    _image = null;
                }
            }
            base.Dispose(disposing);
        }
    }

}

最佳答案

尝试以下方法。这是我为工作中的项目编写的。我去除了一些额外的功能,但是这里除了回答您的问题以外,还有其他更多功能。特别需要注意的是CenterOn和Zoom方法。另请注意,我并没有清除背景,而是先绘制了背景。清除对我也有怪异的副作用。我也继承了Panel,这对我也最有效。
随时将其转换为C#。

Imports System.Drawing.Drawing2D
Imports System
Imports System.Collections
Imports System.ComponentModel
Imports System.Data
Imports System.Drawing
Imports System.Drawing.Imaging
Imports System.IO
Imports System.Runtime.InteropServices
Imports System.Windows.Forms

Public Class ctlViewer
   Inherits Panel

   Protected Const C_SmallChangePercent As Integer = 2
   Protected Const C_LargeChangePercent As Integer = 10

   Protected mimgImage As Image
   Protected mintActiveFrame As Integer
   Protected mdecZoom As Decimal
   Protected mpntUpperLeft As New Point
   Protected mpntCenter As New Point
   Protected mblnDragging As Boolean = False
   Private mButtons As MouseButtons

#Region " Constructor"
   Public Sub New()
      MyBase.New()
      Me.SetStyle(ControlStyles.ContainerControl, False)
      Me.SetStyle(ControlStyles.AllPaintingInWmPaint, True)
      Me.SetStyle(ControlStyles.UserPaint, True)
      Me.SetStyle(ControlStyles.ResizeRedraw, True)
      Me.SetStyle(ControlStyles.UserPaint, True)
      Me.SetStyle(ControlStyles.DoubleBuffer, True)
      ZoomFactor = 1.0
      Me.AutoScroll = True
      Me.BackColor = Color.FromKnownColor(KnownColor.ControlDark)
   End Sub
#End Region

#Region " Properties"

   ''' <summary>
   ''' Image object representing the TIFF image.
   ''' </summary>
   ''' <value></value>
   ''' <returns></returns>
   ''' <remarks></remarks>
   Public Property Image() As Image
      Get
         Return mimgImage
      End Get
      Set(ByVal Value As Image)
         AutoScrollPosition = New Point(0, 0)
         mimgImage = Value
         RaiseEvent ImageLoaded(New ImageLoadedEventArgs(Value))
         UpdateScaleFactor()
         Invalidate()
      End Set
   End Property


   ''' <summary>
   ''' Viewing area of image
   ''' </summary>
   ''' <value></value>
   ''' <returns></returns>
   ''' <remarks></remarks>
   Public ReadOnly Property ViewPort() As Rectangle
      Get
         Dim r As New Rectangle
         Dim pul As Point = Me.CoordViewerToSrc(New Point(0, 0))
         Dim pbr As Point = Me.CoordViewerToSrc(New Point(Me.Width, Me.Height))
         r.Location = pul
         r.Width = pbr.X - pul.X
         r.Height = pbr.Y - pul.Y
         Return r
      End Get
   End Property

   ''' <summary>
   ''' Gets or sets the zoom / scale factor for the image being displayed.
   ''' </summary>
   ''' <value></value>
   ''' <returns></returns>
   ''' <remarks></remarks>
   Public Property ZoomFactor() As Decimal
      Get
         Return mdecZoom
      End Get
      Set(ByVal Value As Decimal)
         If Value < 0 OrElse Value < 0.00001 Then
            Value = 0.00001F
         End If
         mdecZoom = Value
         UpdateScaleFactor()
         Invalidate()
         RaiseEvent ZoomChanged(New ImageViewerEventArgs(Me.Image))
      End Set
   End Property

#End Region

#Region " Event Signatures"
   Public Event ImageMouseDown(ByVal e As ImageMouseEventArgs)
   Public Event ImageMouseMove(ByVal e As ImageMouseEventArgs)
   Public Event ImageMouseUp(ByVal e As ImageMouseEventArgs)
   Public Event ImageLoaded(ByVal e As ImageLoadedEventArgs)
   Public Event ZoomChanged(ByVal e As ImageViewerEventArgs)
   Public Event ImageViewPortChanged(ByVal e As ImageViewerEventArgs)

   Public Event ViewerPaint(ByVal sender As Object, ByVal e As PaintEventArgs)
#End Region

#Region " Public Subs/Functions"

   ''' <summary>
   ''' Pans the viewer by X,Y up to the bounds of the image.
   ''' </summary>
   ''' <param name="x"></param>
   ''' <param name="y"></param>
   ''' <remarks></remarks>
   Public Sub Pan(ByVal x As Integer, ByVal y As Integer)
      Me.AutoScrollPosition = New Point(Math.Abs(Me.AutoScrollPosition.X) + x, Math.Abs(Me.AutoScrollPosition.Y) + y)
      Me.Invalidate()
   End Sub

   ''' <summary>
   ''' Zoom image
   ''' </summary>
   ''' <param name="decZoom"></param>
   ''' <remarks></remarks>
   Public Sub Zoom(ByVal decZoom As Decimal)
      ZoomFactor = decZoom
   End Sub

   ''' <summary>
   ''' Zoom image and scroll to rectangle coordinates.
   ''' </summary>
   ''' <param name="decZoomFactor"></param>
   ''' <param name="objRectangleToCenter"></param>
   ''' <remarks></remarks>
   Public Sub Zoom(ByVal decZoomFactor As Decimal, ByVal objRectangleToCenter As Rectangle)
      Dim intCenterX As Int32 = objRectangleToCenter.X + objRectangleToCenter.Width / 2
      Dim intCenterY As Int32 = objRectangleToCenter.Y + objRectangleToCenter.Height / 2
      Me.CenterOn(New Point(intCenterX, intCenterY))
      Me.ZoomFactor = decZoomFactor
   End Sub

   ''' <summary>
   ''' Zoom to fit image on screen.
   ''' </summary>
   ''' <param name="minZoom"></param>
   ''' <param name="maxZoom"></param>
   ''' <remarks></remarks>
   Public Sub ZoomToFit(ByVal minZoom As Decimal, ByVal maxZoom As Decimal)
      If Not Me.Image Is Nothing Then
         Dim ItoVh As Single = Me.Image.Height / (Me.Height - 2)
         Dim ItoVw As Single = Me.Image.Width / (Me.Width - 2)
         Dim zf As Single = 1 / Math.Max(ItoVh, ItoVw)
         If (((zf > minZoom) And minZoom <> 0) Or minZoom = 0) _
               And ((zf < maxZoom) And maxZoom <> 0) Or maxZoom = 0 Then
            Me.Zoom(zf)
         End If
      End If
   End Sub

   ''' <summary>
   ''' Zoom to fit width of image
   ''' </summary>
   ''' <param name="minZoom"></param>
   ''' <param name="maxZoom"></param>
   ''' <remarks></remarks>
   Public Sub ZoomToWidth(ByVal minZoom As Decimal, ByVal maxZoom As Decimal)
      If Image Is Nothing Then
         Me.AutoScrollMargin = Me.Size
         Me.AutoScrollMinSize = Me.Size

         mpntCenter = New Point(0, 0)
         mpntUpperLeft = New Point(0, 0)
         Exit Sub
      End If
      Dim intOff As Integer = 0
      If ScrollStateVScrollVisible Then
         intOff = ScrollStateVScrollVisible
      End If
      Dim ItoVw As Single = Me.Image.Width / (Me.Width - 2)
      Dim zf As Single = 1 / ItoVw
      If (Me.Image.Height * zf) >= Me.Height Then
         ItoVw = Me.Image.Width / (Me.Width - 22)
         zf = 1 / ItoVw
      End If
      If (((zf > minZoom) And minZoom <> 0) Or minZoom = 0) _
            And ((zf < maxZoom) And maxZoom <> 0) Or maxZoom = 0 Then
         Me.Zoom(zf)
      End If
   End Sub

   ''' <summary>
   ''' Adjust scrollbars to zoomed size of image
   ''' </summary>
   ''' <remarks></remarks>
   Protected Sub UpdateScaleFactor()
      If Image Is Nothing Then
         Me.AutoScrollMargin = Me.Size
         Me.AutoScrollMinSize = Me.Size

         mpntCenter = New Point(0, 0)
         mpntUpperLeft = New Point(0, 0)
      Else
         Me.AutoScrollMinSize = New Size(CInt(Me.Image.Width * ZoomFactor + 0.5F), CInt(Me.Image.Height * ZoomFactor + 0.5F))
      End If
      Me.HorizontalScroll.LargeChange = Me.Size.Width * (C_LargeChangePercent / 100)
      Me.VerticalScroll.LargeChange = Me.Size.Height * (C_LargeChangePercent / 100)
      Me.HorizontalScroll.SmallChange = Me.Size.Width * (C_SmallChangePercent / 100)
      Me.VerticalScroll.SmallChange = Me.Size.Height * (C_SmallChangePercent / 100)
   End Sub

   ''' <summary>
   ''' Convert a point of the original image to screen coordinates adjusted for zoom and pan.
   ''' </summary>
   ''' <param name="pntPoint"></param>
   ''' <returns></returns>
   ''' <remarks></remarks>
   Public Function CoordSrcToViewer(ByVal pntPoint As Point) As Point
      Dim pntResult As New Point
      pntResult.X = pntPoint.X * Me.ZoomFactor + Me.AutoScrollPosition.X
      pntResult.Y = pntPoint.Y * Me.ZoomFactor + Me.AutoScrollPosition.Y
      Return pntResult
   End Function

   ''' <summary>
   ''' Convert a screen point to the corrseponding coordinate of the original image.
   ''' </summary>
   ''' <param name="pntPoint"></param>
   ''' <returns></returns>
   ''' <remarks></remarks>
   Public Function CoordViewerToSrc(ByVal pntPoint As Point) As Point
      Dim pntResult As New Point
      pntResult.X = (pntPoint.X - Me.AutoScrollPosition.X) / Me.ZoomFactor
      pntResult.Y = (pntPoint.Y - Me.AutoScrollPosition.Y) / Me.ZoomFactor
      Return pntResult
   End Function

   ''' <summary>
   ''' Returns an offset needed to move the center point to make visible.
   ''' </summary>
   ''' <param name="imagePoint"></param>
   ''' <returns></returns>
   ''' <remarks></remarks>
   Friend Function PointIsVisible(ByVal imagePoint As Point) As Point
      Dim pntViewer As Point = Me.CoordSrcToViewer(imagePoint)
      Dim pntSize As New Point((pntViewer.X - Me.Width) / Me.ZoomFactor, (pntViewer.Y - Me.Height) / Me.ZoomFactor)
      If pntViewer.X > 0 And pntViewer.X < Me.Width Then
         pntSize.X = 0
      End If
      If pntViewer.Y > 0 And pntViewer.Y < Me.Height Then
         pntSize.Y = 0
      End If
      If pntViewer.X < 0 Then
         pntSize.X = pntViewer.X
      End If
      If pntViewer.Y < 0 Then
         pntSize.Y = pntViewer.Y
      End If
      Return pntSize
   End Function

   ''' <summary>
   ''' Centers view on coordinates of the original image.
   ''' </summary>
   ''' <param name="X"></param>
   ''' <param name="Y"></param>
   ''' <remarks></remarks>
   Public Sub CenterOn(ByVal X As Integer, ByVal Y As Integer)
      CenterOn(New Point(X, Y))
   End Sub

   ''' <summary>
   ''' Centers view on a point of the original image.
   ''' </summary>
   ''' <param name="pntCenter"></param>
   ''' <remarks></remarks>
   Public Sub CenterOn(ByVal pntCenter As Point)
      Dim midX As Integer = Me.Width / 2
      Dim midY As Integer = Me.Height / 2
      Dim intX As Integer = (pntCenter.X * ZoomFactor - midX)
      Dim intY As Integer = (pntCenter.Y * ZoomFactor - midY)
      Me.AutoScrollPosition = New Point(intX, intY)
      Me.Invalidate()
   End Sub

   ''' <summary>
   ''' Returns image coordinate which is centered in viewer.
   ''' </summary>
   ''' <returns></returns>
   ''' <remarks></remarks>
   Public Function GetCenterPoint() As Point
      Dim pntResult As Point
      pntResult = CoordViewerToSrc(New Point(Me.Width / 2, Me.Height / 2))
      If pntResult.X > Me.Image.Width Or pntResult.Y > Image.Height Then
         pntResult = Nothing
      End If
      Return pntResult
   End Function

   ''' <summary>
   ''' Fire viewport changed event.
   ''' </summary>
   ''' <remarks></remarks>
   Private Sub FireViewPortChangedEvent()
      Dim e As New ImageViewerEventArgs(Me.Image)
      RaiseEvent ImageViewPortChanged(e)
   End Sub
   Private Sub FireViewerPaintEvent(ByVal e As PaintEventArgs)
      RaiseEvent ViewerPaint(Me, e)
   End Sub
#End Region

#Region " Overrides"

   ''' <summary>
   ''' Paint image in proper position and zoom.  All work is done with a Matrix object.
   ''' The coordinates of the graphics instance of the ctlViewer_OnPaint event
   ''' are transformed.  This allows drawing on the "paper image" rather than "over the viewport"
   ''' </summary>
   ''' <param name="e"></param>
   ''' <remarks></remarks>
   Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
      If mimgImage Is Nothing Then
         MyBase.OnPaintBackground(e)
         Return
      Else
         Debug.WriteLine("ctl painting")
         Dim mx As New Matrix
         e.Graphics.FillRectangle(New SolidBrush(Me.BackColor), 0, 0, Me.Width, Me.Height)
         mx.Translate(Me.AutoScrollPosition.X, Me.AutoScrollPosition.Y)
         mx.Scale(ZoomFactor, ZoomFactor)
         e.Graphics.SetClip(New Rectangle(0, 0, Me.Width, Me.Height))
         e.Graphics.InterpolationMode = InterpolationMode.Low
         e.Graphics.SmoothingMode = SmoothingMode.HighSpeed
         e.Graphics.Transform = mx
         Dim ia As New ImageAttributes
         e.Graphics.DrawImage(Image, _
             New Rectangle(-Me.AutoScrollPosition.X / ZoomFactor, _
             -Me.AutoScrollPosition.Y / ZoomFactor, _
              Me.Width / ZoomFactor, _
              Me.Height / ZoomFactor), _
              Me.ViewPort.Left, Me.ViewPort.Top, Me.ViewPort.Width, Me.ViewPort.Height, _
             GraphicsUnit.Pixel, ia)
         ia.Dispose()
      End If
      Me.mpntCenter = Me.GetCenterPoint
      FireViewPortChangedEvent()
      MyBase.OnPaint(e)
      e.Graphics.ResetClip()
      e.Graphics.ResetTransform()
      Me.FireViewerPaintEvent(e)

   End Sub

   ''' <summary>
   ''' Pan image and raise event.
   ''' </summary>
   ''' <param name="e"></param>
   ''' <remarks></remarks>
   Protected Overrides Sub OnMouseDown(ByVal e As System.Windows.Forms.MouseEventArgs)
      Me.mButtons = e.Button
      RaiseEvent ImageMouseDown(New ImageMouseEventArgs(e.Button, e.Clicks, e.X, e.Y, e.Delta, Me.ZoomFactor, Me.AutoScrollPosition))
      MyBase.OnMouseDown(e)
   End Sub

   ''' <summary>
   ''' Stop panning image and raise event.
   ''' </summary>
   ''' <param name="e"></param>
   ''' <remarks></remarks>
   Protected Overrides Sub OnMouseUp(ByVal e As System.Windows.Forms.MouseEventArgs)
      Me.Cursor = Cursors.Arrow
      MyBase.OnMouseUp(e)

      RaiseEvent ImageMouseUp(New ImageMouseEventArgs(e.Button, e.Clicks, e.X, e.Y, e.Delta, Me.ZoomFactor, Me.AutoScrollPosition))
      Me.mButtons = Windows.Forms.MouseButtons.None
   End Sub

   ''' <summary>
   ''' Pan image if PanOnMouseMove is True.  Fire the ImageMouseMove event.
   ''' </summary>
   ''' <param name="e"></param>
   ''' <remarks></remarks>
   Protected Overrides Sub OnMouseMove(ByVal e As System.Windows.Forms.MouseEventArgs)
      Static oldX As Integer
      Static oldy As Integer
      Try
         oldX = e.X
         oldy = e.Y
      Catch ex As Exception
         Throw ex
      Finally
         MyBase.OnMouseMove(e)
         RaiseEvent ImageMouseMove(New ImageMouseEventArgs(Me.mButtons, e.Clicks, e.X, e.Y, e.Delta, Me.ZoomFactor, Me.AutoScrollPosition))
      End Try
   End Sub

   ''' <summary>
   ''' Catch a panel scroll event.
   ''' </summary>
   ''' <param name="m"></param>
   ''' <remarks></remarks>
   Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
      Const WM_VSCROLL As Integer = 277 '115 hex
      Const WM_HSCROLL As Integer = 276 '0x114;
      MyBase.WndProc(m)
      If Not m.HWnd.Equals(Me.Handle) Then
         Return
      End If
      If m.Msg = WM_VSCROLL Or m.Msg = WM_HSCROLL Then
         Me.Invalidate()
      End If
   End Sub
#End Region
End Class

08-17 15:21