2009-02-05 38 views
3

我正在尝试编写一个自定义可滚动的c#控件,用于放大图像上的特定点。我面临的问题是启用双缓冲时,图像似乎朝左上角飞跃,然后正确放大至鼠标点击的位置。这似乎只发生在我设置AutoScrollPosition时。我证实它不会发生在我的OnPaint方法中。这似乎是我无法追查的一些内部行为。有没有人解决过这个问题?缩放到c中的一个点#

下面是一些示例代码,演示了我试图完成的任务。当图像相当大时,这个问题似乎只能向用户明显呈现。

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); 
     } 
    } 

} 

回答

1

请尝试以下操作。这是我为在工作中使用的项目而编写的。我删除了一些额外的功能,但这里有更多的东西需要回答你的问题。特别值得注意的是CenterOn和Zoom方法。另外请注意,我没有清除背景,但是先画背景。清除对我来说也有奇怪的副作用。我也是从最适合我的面板继承而来的。 随意将其转换为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