可拖动的选择矩形

本文关键字:选择 拖动 | 更新日期: 2023-09-27 18:16:17

在有人指出之前,我知道这里已经有人问过一个相同标题的问题,我认为它不能回答我的问题。

在这个问题中,我正在制作一个区域选择组件来选择图片上的一个区域。使用自定义控件显示该图片,该控件在OnPaint期间绘制该图片。

我的选择矩形的代码如下:

internal class AreaSelection : Control
{
    private Rectangle selection
    {
        get { return new Rectangle(Point.Empty, Size.Subtract(this.Size, new Size(1, 1))); }
    }
    private Size mouseStartLocation;
    public AreaSelection()
    {
        this.Size = new Size(150, 150);
        this.SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.SupportsTransparentBackColor, true);
        this.BackColor = Color.FromArgb(70, 200, 200, 200);
    }
    protected override void OnMouseEnter(EventArgs e)
    {
        this.Cursor = Cursors.SizeAll;
        base.OnMouseEnter(e);
    }
    protected override void OnMouseDown(MouseEventArgs e)
    {
        this.mouseStartLocation = new Size(e.Location);
        base.OnMouseDown(e);
    }
    protected override void OnMouseMove(MouseEventArgs e)
    {
        if (e.Button == MouseButtons.Left)
        {
            Point offset = e.Location - this.mouseStartLocation;
            this.Left += offset.X;
            this.Top += offset.Y;
        }
        base.OnMouseMove(e);
    }
    protected override void OnPaint(PaintEventArgs e)
    {
        e.Graphics.DrawRectangle(new Pen(Color.Black) { DashStyle = DashStyle.Dash }, this.selection);
        Debug.WriteLine("Selection redrawn");
    }
}

这样就有了一个半透明的矩形,我可以拖动它。我遇到的问题是,当拖动通过矩形显示的底层图像时,会滞后于矩形的位置。

我移动矩形的速度越快,这一点就越明显。当我停止移动它的时候,图像就会跟上,一切又会完美地对齐。我猜想矩形画的方式有问题,但我真的不知道是什么……如有任何帮助,不胜感激。

编辑:

我注意到,当我拖动选择区域时,查看器被重绘的频率是选择区域的两倍。这可能是问题的原因吗?

编辑2:

下面是查看器的代码,如果它是相关的:

public enum ImageViewerViewMode
{
    Normal,
    PrintSelection,
    PrintPreview
}
public enum ImageViewerZoomMode
{
    None,
    OnClick,
    Lens
}
public partial class ImageViewer : UserControl
{
    /// <summary>
    /// The current zoom factor. Note: Use SetZoom() to set the value.
    /// </summary>
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public float ZoomFactor
    {
        get { return this.zoomFactor; }
        private set
        {
            this.zoomFactor = value;
        }
    }
    /// <summary>
    /// The maximum zoom factor to use
    /// </summary>
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public float MaximumZoomFactor
    {
        get
        {
            return this.maximumZoomFactor;
        }
        set
        {
            this.maximumZoomFactor = value;
            this.SetZoomFactorLimits();
        }
    }
    /// <summary>
    /// The minimum zoom factort to use
    /// </summary>
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public float MinimumZoomFactor
    {
        get
        {
            return this.minimumZoomFactor;
        }
        set
        {
            this.minimumZoomFactor = value;
            this.SetZoomFactorLimits();
        }
    }
    /// <summary>
    /// The multiplying factor to apply to each ZoomIn/ZoomOut command
    /// </summary>
    [Category("Behavior")]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    [DefaultValue(2F)]
    public float ZoomStep { get; set; }
    /// <summary>
    /// The image currently displayed by the control
    /// </summary>
    [Category("Data")]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public Image Image
    {
        get { return this.image; }
        set
        {
            this.image = value;
            this.ZoomExtents();
            this.minimumZoomFactor = this.zoomFactor / 10;
            this.MaximumZoomFactor = this.zoomFactor * 10;
        }
    }
    public ImageViewerViewMode ViewMode { get; set; }
    public ImageViewerZoomMode ZoomMode { get; set; }
    private ImageViewerLens Lens { get; set; }
    private float zoomFactor;
    private float minimumZoomFactor;
    private float maximumZoomFactor;
    private bool panning;
    private Point imageLocation;
    private Point imageTranslation;
    private Image image;
    private AreaSelection areaSelection;
    /// <summary>
    /// Class constructor
    /// </summary>
    public ImageViewer()
    {
        this.DoubleBuffered = true;
        this.MinimumZoomFactor = 0.1F;
        this.MaximumZoomFactor = 10F;
        this.ZoomStep = 2F;
        this.UseScannerUI = true;
        this.Lens = new ImageViewerLens();
        this.ViewMode = ImageViewerViewMode.PrintSelection;
        this.areaSelection = new AreaSelection();
        this.Controls.Add(this.areaSelection);
        // TWAIN
        // Initialise twain
        this.twain = new Twain(new WinFormsWindowMessageHook(this));
        // Try to set the last used default scanner
        if (this.AvailableScanners.Any())
        {
            this.twain.TransferImage += twain_TransferImage;
            this.twain.ScanningComplete += twain_ScanningComplete;
            if (!this.SetScanner(this.defaultScanner))
                this.SetScanner(this.AvailableScanners.First());
        }
    }
    /// <summary>
    /// Saves the currently loaded image under the specified filename, in the specified format at the specified quality
    /// </summary>
    /// <param name="FileName">The file name (full file path) under which to save the file. File type extension is not required.</param>
    /// <param name="Format">The file format under which to save the file</param>
    /// <param name="Quality">The quality in percent of the image to save. This is optional and may or may not be used have an effect depending on the chosen file type. Default is maximum quality.</param>
    public void SaveImage(string FileName, GraphicFormats Format, uint Quality = 100)
    {
        ImageCodecInfo encoder;
        EncoderParameters encoderParameters;
        if (FileName.IsNullOrEmpty())
            throw new ArgumentNullException(FileName);
        else
        {
            string extension = Path.GetExtension(FileName);
            if (!string.IsNullOrEmpty(extension))
                FileName = FileName.Replace(extension, string.Empty);
            FileName += "." + Format.ToString();
        }
        Quality = Math.Min(Math.Max(1, Quality), 100);
        if (!TryGetEncoder(Format, out encoder))
            return;
        encoderParameters = new EncoderParameters(1);
        encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, (int)Quality);
        this.Image.Save(FileName, encoder, encoderParameters);
    }
    /// <summary>
    /// Tries to retrieve the appropriate encoder for the chose image format.
    /// </summary>
    /// <param name="Format">The image format for which to attempt retrieving the encoder</param>
    /// <param name="Encoder">The encoder object in which to store the encoder if found</param>
    /// <returns>True if the encoder was found, else false</returns>
    private bool TryGetEncoder(GraphicFormats Format, out ImageCodecInfo Encoder)
    {
        ImageCodecInfo[] codecs;
        codecs = ImageCodecInfo.GetImageEncoders();
        Encoder = codecs.First(c => c.FormatDescription.Equals(Format.ToString(), StringComparison.CurrentCultureIgnoreCase));
        return Encoder != null;
    }
    /// <summary>
    /// Set the zoom level to view the entire image in the control
    /// </summary>
    public void ZoomExtents()
    {
        if (this.Image == null)
            return;
        this.ZoomFactor = (float)Math.Min((double)this.Width / this.Image.Width, (double)this.Height / this.Image.Height);
        this.LimitBasePoint(imageLocation.X, imageLocation.Y);
        this.Invalidate();
    }
    /// <summary>
    /// Multiply the zoom
    /// </summary>
    /// <param name="NewZoomFactor">The zoom factor to set for the image</param>
    public void SetZoom(float NewZoomFactor)
    {
        this.SetZoom(NewZoomFactor, Point.Empty);
    }
    /// <summary>
    /// Multiply the zoom
    /// </summary>
    /// <param name="NewZoomFactor">The zoom factor to set for the image</param>
    /// <param name="ZoomLocation">The point in which to zoom in</param>
    public void SetZoom(float NewZoomFactor, Point ZoomLocation)
    {
        int x;
        int y;
        float multiplier;
        multiplier = NewZoomFactor / this.ZoomFactor;
        x = (int)((ZoomLocation.IsEmpty ? this.Width / 2 : ZoomLocation.X - imageLocation.X) / ZoomFactor);
        y = (int)((ZoomLocation.IsEmpty ? this.Height / 2 : ZoomLocation.Y - imageLocation.Y) / ZoomFactor);
        if ((multiplier < 1 && this.ZoomFactor > this.MinimumZoomFactor) || (multiplier > 1 && this.ZoomFactor < this.MaximumZoomFactor))
            ZoomFactor *= multiplier;
        else
            return;
        LimitBasePoint((int)(this.Width / 2 - x * ZoomFactor), (int)(this.Height / 2 - y * ZoomFactor));
        this.Invalidate();
    }
    /// <summary>
    /// Determines the base point for positioning the image
    /// </summary>
    /// <param name="x">The x coordinate based on which to determine the positioning</param>
    /// <param name="y">The y coordinate based on which to determine the positioning</param>
    private void LimitBasePoint(int x, int y)
    {
        int width;
        int height;
        if (this.Image == null)
            return;
        width = this.Width - (int)(Image.Width * ZoomFactor);
        height = this.Height - (int)(Image.Height * ZoomFactor);
        x = width < 0 ? Math.Max(Math.Min(x, 0), width) : width / 2;
        y = height < 0 ? Math.Max(Math.Min(y, 0), height) : height / 2;
        imageLocation = new Point(x, y);
    }
    /// <summary>
    /// Verify that the maximum and minimum zoom are correctly set
    /// </summary>
    private void SetZoomFactorLimits()
    {
        float maximum = this.MaximumZoomFactor;
        float minimum = this.minimumZoomFactor;
        this.maximumZoomFactor = Math.Max(maximum, minimum);
        this.minimumZoomFactor = Math.Min(maximum, minimum);
    }
    /// <summary>
    /// Mouse button down event
    /// </summary>
    protected override void OnMouseDown(MouseEventArgs e)
    {
        switch (this.ZoomMode)
        {
            case ImageViewerZoomMode.OnClick:
                switch (e.Button)
                {
                    case MouseButtons.Left:
                        this.SetZoom(this.ZoomFactor * this.ZoomStep, e.Location);
                        break;
                    case MouseButtons.Middle:
                        this.panning = true;
                        this.Cursor = Cursors.NoMove2D;
                        this.imageTranslation = e.Location;
                        break;
                    case MouseButtons.Right:
                        this.SetZoom(this.ZoomFactor / this.ZoomStep, e.Location);
                        break;
                }
                break;
            case ImageViewerZoomMode.Lens:
                if (e.Button == MouseButtons.Left)
                {
                    this.Cursor = Cursors.Cross;
                    this.Lens.Location = e.Location;
                    this.Lens.Visible = true;
                }
                else
                {
                    this.Cursor = Cursors.Default;
                    this.Lens.Visible = false;
                }
                this.Invalidate();
                break;
        }
        base.OnMouseDown(e);
    }
    /// <summary>
    /// Mouse button up event
    /// </summary>
    protected override void OnMouseUp(MouseEventArgs e)
    {
        switch (this.ZoomMode)
        {
            case ImageViewerZoomMode.OnClick:
                if (e.Button == MouseButtons.Middle)
                {
                    panning = false;
                    this.Cursor = Cursors.Default;
                }
                break;
            case ImageViewerZoomMode.Lens:
                break;
        }
        base.OnMouseUp(e);
    }
    /// <summary>
    /// Mouse move event
    /// </summary>
    protected override void OnMouseMove(MouseEventArgs e)
    {
        switch (this.ViewMode)
        {
            case ImageViewerViewMode.Normal:
                switch (this.ZoomMode)
                {
                    case ImageViewerZoomMode.OnClick:
                        if (panning)
                        {
                            LimitBasePoint(imageLocation.X + e.X - this.imageTranslation.X, imageLocation.Y + e.Y - this.imageTranslation.Y);
                            this.imageTranslation = e.Location;
                        }
                        break;
                    case ImageViewerZoomMode.Lens:
                        if (this.Lens.Visible)
                        {
                            this.Lens.Location = e.Location;
                        }
                        break;
                }
                break;
            case ImageViewerViewMode.PrintSelection:
                break;
            case ImageViewerViewMode.PrintPreview:
                break;
        }
        base.OnMouseMove(e);
    }
    /// <summary>
    /// Resize event
    /// </summary>
    protected override void OnResize(EventArgs e)
    {
        LimitBasePoint(imageLocation.X, imageLocation.Y);
        this.Invalidate();
        base.OnResize(e);
    }
    /// <summary>
    /// Paint event
    /// </summary>
    protected override void OnPaint(PaintEventArgs pe)
    {
        Rectangle src;
        Rectangle dst;
        pe.Graphics.Clear(this.BackColor);
        if (this.Image != null)
        {
            switch (this.ViewMode)
            {
                case ImageViewerViewMode.Normal:
                    src = new Rectangle(Point.Empty, new Size(Image.Width, Image.Height));
                    dst = new Rectangle(this.imageLocation, new Size((int)(this.Image.Width * this.ZoomFactor), (int)(this.Image.Height * this.ZoomFactor)));
                    pe.Graphics.DrawImage(this.Image, dst, src, GraphicsUnit.Pixel);
                    this.Lens.Draw(pe.Graphics, this.Image, this.ZoomFactor, this.imageLocation);
                    break;
                case ImageViewerViewMode.PrintSelection:
                    src = new Rectangle(Point.Empty, new Size(Image.Width, Image.Height));
                    dst = new Rectangle(this.imageLocation, new Size((int)(this.Image.Width * this.ZoomFactor), (int)(this.Image.Height * this.ZoomFactor)));
                    pe.Graphics.DrawImage(this.Image, dst, src, GraphicsUnit.Pixel);
                    break;
                case ImageViewerViewMode.PrintPreview:
                    break;
            }
        }
        //Debug.WriteLine("Viewer redrawn " + DateTime.Now);
        base.OnPaint(pe);
    }
}
编辑3:

在将高度设置为较大时将遇到更多与图形相关的问题。例如,如果在AreaSelection构造器中我将高度设置为500,那么拖动控件确实会破坏绘画。

可拖动的选择矩形

当拖动显示在矩形中的底层图像时,会滞后于

这是相当不可避免的,更新矩形也会重新绘制图像。如果这是昂贵的,比如超过30毫秒,那么眼睛就会注意到这一点。

对于像现代机器上的图像这样简单的东西来说,这是很多毫秒。唯一可能需要这么长时间的情况是当图像并且需要重新缩放以适合图片框时。并且像素格式与视频适配器的像素格式不兼容,因此它们中的每一个都必须从图像像素格式转换为视频适配器的像素格式。这确实可以加起来多毫秒。

你需要帮助避免PictureBox每次绘制图像时都要消耗那么多cpu周期。通过预缩放图像,将其从巨大的位图转变为更适合控件的位图。通过改变像素格式,32bppPArgb格式是最好的,因为它与绝大多数视频适配器的像素格式相匹配。它绘制的速度比所有其他格式快10倍。您将在这个答案中找到进行此转换的样板代码。