1

在有人指出之前,我知道这里已经提出了一个同名的问题,它只是没有回答我认为的问题。

在 .NET 3.5 中工作 就像在那个问题中一样,我正在制作一个区域选择组件来选择图片上的一个区域。图片使用自定义控件显示,其中图片在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,那么拖动控件真的会搞砸绘画。

4

1 回答 1

1

在拖动通过矩形显示的底层图像时会落后

这是不可避免的,更新矩形也会重绘图像。如果这很昂贵,比如超过 30 毫秒,那么这可能会变得很明显。

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

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

于 2013-05-31T09:06:18.353 回答