我试图编写一个自定义的可滚动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