0

I have implemented a rubber band by adopting the following code:

https://support.microsoft.com/en-gb/kb/314945

This is my code:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Data;
using System.Text;
using System.Windows.Forms;
using System.IO;
using System.Reflection;

namespace Test
{
    public partial class TestX: UserControl
    {
        private Boolean m_bLeftButton { get; set; }
        private Boolean m_bMiddleButton { get; set; }
        private Boolean m_bZoomWindow { get; set; }

        Point m_ptOriginal = new Point();
        Point m_ptLast = new Point();

        public TestX()
        {
            m_bZoomWindow = false;
            m_bLeftButton = false;
            m_bMiddleButton = false;
        }

        // Called when the left mouse button is pressed. 
        public void MyMouseDown(Object sender, MouseEventArgs e)
        {
            if(m_bZoomWindow && e.Button == MouseButtons.Left)
            {
                // Make a note that we "have the mouse".
                m_bLeftButton = true;

                // Store the "starting point" for this rubber-band rectangle.
                m_ptOriginal.X = e.X;
                m_ptOriginal.Y = e.Y;
                // Special value lets us know that no previous
                // rectangle needs to be erased.
                m_ptLast.X = -1;
                m_ptLast.Y = -1;
            }
        }

        // Convert and normalize the points and draw the reversible frame.
        private void MyDrawReversibleRectangle(Point p1, Point p2)
        {
            Rectangle rc = new Rectangle();

            // Convert the points to screen coordinates.
            p1 = PointToScreen(p1);
            p2 = PointToScreen(p2);
            // Normalize the rectangle.
            if (p1.X < p2.X)
            {
                rc.X = p1.X;
                rc.Width = p2.X - p1.X;
            }
            else
            {
                rc.X = p2.X;
                rc.Width = p1.X - p2.X;
            }
            if (p1.Y < p2.Y)
            {
                rc.Y = p1.Y;
                rc.Height = p2.Y - p1.Y;
            }
            else
            {
                rc.Y = p2.Y;
                rc.Height = p1.Y - p2.Y;
            }
            // Draw the reversible frame.
            ControlPaint.DrawReversibleFrame(rc,
                            Color.WhiteSmoke, FrameStyle.Thick);
        }

        // Called when the left mouse button is released.
        public void MyMouseUp(Object sender, MouseEventArgs e)
        {
            if(m_bZoomWindow && e.Button == MouseButtons.Left)
            {
                // Set internal flag to know we no longer "have the mouse".
                m_bZoomWindow = false;
                m_bLeftButton = false;

                // If we have drawn previously, draw again in that spot
                // to remove the lines.
                if (m_ptLast.X != -1)
                {
                    Point ptCurrent = new Point(e.X, e.Y);
                    MyDrawReversibleRectangle(m_ptOriginal, m_ptLast);

                    // Do zoom now ...
                }
                // Set flags to know that there is no "previous" line to reverse.
                m_ptLast.X = -1;
                m_ptLast.Y = -1;
                m_ptOriginal.X = -1;
                m_ptOriginal.Y = -1;
            }
        }

        // Called when the mouse is moved.
        public void MyMouseMove(Object sender, MouseEventArgs e)
        {
            Point ptCurrent = new Point(e.X, e.Y);

            if(m_bLeftButton)
            {
                // If we "have the mouse", then we draw our lines.
                if (m_bZoomWindow)
                {
                    // If we have drawn previously, draw again in
                    // that spot to remove the lines.
                    if (m_ptLast.X != -1)
                    {
                        MyDrawReversibleRectangle(m_ptOriginal, m_ptLast);
                    }
                    // Update last point.
                    if(ptCurrent != m_ptLast)
                    {
                        m_ptLast = ptCurrent;
                        // Draw new lines.
                        MyDrawReversibleRectangle(m_ptOriginal, ptCurrent);
                    }
                }
            }
        }

        // Set up delegates for mouse events.
        protected override void OnLoad(System.EventArgs e)
        {
            MouseDown += new MouseEventHandler(MyMouseDown);
            MouseUp += new MouseEventHandler(MyMouseUp);
            MouseMove += new MouseEventHandler(MyMouseMove);
            MouseWheel += new MouseEventHandler(MyMouseWheel);

            m_bZoomWindow = false;
        }
    }
}

It itself it works, but the rectangle flashes. Other programs, like CAD packages, have zero flickering when drawing a rectangle.

My form is set to use DoubleBuffering so I thought it would be OK. Has anyone else encountered this issue?

Update: I thought I would go back to the beginning and do a test winforms project with a table layout panel and a embedded user control. I set the user control to work like the answer I was provided. The only difference was that I set the user control constructor like this:

public MyUserControl()
{
    InitializeComponent();

    _selectionPen = new Pen(Color.Black, 3.0f);

    SetStyle(ControlStyles.OptimizedDoubleBuffer | 
             ControlStyles.AllPaintingInWmPaint | 
             ControlStyles.UserPaint, true);
    BackColor = Color.Transparent;
    Dock = DockStyle.Fill;
    Margin = new Padding(1);
}

Notice the additional control styles AllPaintingInWmPaint and UserPaint? It seems that I needed these in addition to the OptimizedDoubleBuffer style. I also set the form as double buffered.

When I make those adjustments I can draw a flicker free rubber band. Hoorah! But when I add back in the embedded class for rendering a DWG in the user control, I get the conflict of one over imposing the other.

So that is where I am at and I will wait to see what I can glean from the vendors of the DWG viewer class.

Andrew Truckle
  • 17,769
  • 16
  • 66
  • 164
  • I removed that dup linked (first time I did that) since I think the OP is trying to rubberband the desktop or multiple controls. – LarsTech May 31 '16 at 15:28
  • My form has a TableLayoutPanel which has a UserControl which is docked to fill the panel. The UserControl does also render a drawing (I have removed this code for clarity). The user is dragging a rectangle over the one UserControl object. – Andrew Truckle May 31 '16 at 15:31
  • 1
    You are drawing the rectangle on `MouseMove`? I would just keep track of the coordinates in `MouseMove` and move the actual draw call to the `Paint` event. If an action invalidates the form during the mouse move, paint will be called, which will probably overwrite your rectangle otherwise. You can always call `Invalidate` from your `MouseMove` to force redraw. – jimbobmcgee May 31 '16 at 15:44
  • I disagree, @jimbobmcgee. I have code that is structured precisely as OP describes, and it is a solid technique. This is one of the exceptions to the rule. – DonBoitnott May 31 '16 at 15:49
  • @DonBoitnott - I will believe it when I see it ;-) – jimbobmcgee May 31 '16 at 16:13
  • @jimbobmcgee Whilst I am moving my mouse I can't do another action. If you feel there is a better way please provide an answer and I will try it. Thanks. – Andrew Truckle May 31 '16 at 16:24
  • 'You' personally, or 'you' your code? Windows is doing many actions while you are moving your mouse -- one of those is painting your form – jimbobmcgee May 31 '16 at 17:05
  • 1
    @DonBoitnott -- I concede, the `ControlPaint.DrawReversibleFrame` binds its `Graphics` to the Desktop, not the form, so handles repainting differently. – jimbobmcgee May 31 '16 at 18:29

1 Answers1

1

To demonstrate using the Paint event, from my comment to the OP.

Smoothest box draw I was able to get was by doing it in Paint and setting ControlStyles.OptimizeDoubleBuffer on the control. Of course, it depends on the intended bounds of your box -- this will not exceed the bounds of the control itself (i.e. will not draw onto the form or desktop):

using System.Drawing;
using System.Windows.Forms;

namespace WinformsScratch.RubberBand
{
    public class TestY : Control
    {
        private Point? _selectionStart;
        private Point? _selectionEnd;

        private readonly Pen _selectionPen;

        public TestY()
        {
            _selectionPen = new Pen(Color.Black, 3.0f);

            SetStyle(ControlStyles.OptimizedDoubleBuffer, true);

            MouseDown += (s, e) => {
                if (e.Button == MouseButtons.Left) 
                    _selectionStart = _selectionEnd = e.Location;
            };

            MouseUp += (s, e) => {
                if (e.Button == MouseButtons.Left) 
                {
                    _selectionStart = _selectionEnd = null; 
                    Invalidate(false); 
                }
            };

            MouseMove += (s, e) => {
                if (_selectionStart.HasValue &&
                    _selectionEnd.HasValue &&
                    _selectionEnd.Value != e.Location)
                {
                    _selectionEnd = e.Location;
                    Invalidate(false);
                }
            };

            Paint += (s, e) => {
                if (_selectionStart.HasValue && _selectionEnd.HasValue)
                    e.Graphics.DrawRectangle(_selectionPen, GetSelectionRectangle());
            };
        }

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

        private Rectangle GetSelectionRectangle()
        {
            Rectangle rc = new Rectangle();

            if (_selectionStart.HasValue && _selectionEnd.HasValue)
            {
                // Normalize the rectangle.
                if (_selectionStart.Value.X < _selectionEnd.Value.X)
                {
                    rc.X = _selectionStart.Value.X;
                    rc.Width = _selectionEnd.Value.X - _selectionStart.Value.X;
                }
                else
                {
                    rc.X = _selectionEnd.Value.X;
                    rc.Width = _selectionStart.Value.X - _selectionEnd.Value.X;
                }

                if (_selectionStart.Value.Y < _selectionEnd.Value.Y)
                {
                    rc.Y = _selectionStart.Value.Y;
                    rc.Height = _selectionEnd.Value.Y - _selectionStart.Value.Y;
                }
                else
                {
                    rc.Y = _selectionEnd.Value.Y;
                    rc.Height = _selectionStart.Value.Y - _selectionEnd.Value.Y;
                }
            }

            return rc;
        }
    }
}
jimbobmcgee
  • 1,561
  • 11
  • 34
  • Thanks. I will try this rubber band rectangle class in the morning! As for intended bounds, it is basically a temporary zoom window. So once it is drawn it is going to vanish when the new data is rendered. – Andrew Truckle May 31 '16 at 18:39
  • There may be some z-ordering issues to overcome, so you may need to overlay it on the thing you are zooming into, possibly by making the base colour of the control transparent and using `this.BringToFront()` to render it above. – jimbobmcgee May 31 '16 at 19:13
  • I appreciate you help. I did add it, but as you thought, I have a battle between teh drawing of the rectangle and the interval view object rendering its data. So there is a fight going on between the two which might have been the problem all along. So, are you proposing a dedicated control just for rendering the rectangle? It is just that the mouse move events etc. that I get are only for the usercontrol object. – Andrew Truckle May 31 '16 at 19:36
  • It depends what you are zooming into. It might be more appropriate to just put the rectangle-drawing code into the 'zoomable window' itself. – jimbobmcgee May 31 '16 at 19:39
  • It is a view object of a DWG file. – Andrew Truckle May 31 '16 at 19:41
  • For instance, a `Panel` with a `BackgroundImage` property set could have this control added to it with `Dock = DockStyle.Fill`, and the box would draw over the image properly. However, if the `Panel` had other controls in it, it would probably draw underneath them (or miss them completely, because WinForms can't do 'true' transparency) – jimbobmcgee May 31 '16 at 19:48
  • I'm not sure what a DWG file (and associated view object) actually is but, if you can subclass the control (i.e. it is not `sealed`) you should be able to override `OnPaint` instead of the event -- you just have to make sure `base.OnPaint` is called first... – jimbobmcgee May 31 '16 at 19:54
  • Hi jimbobmcgee. Can I please email you about somethign to do with this? – Andrew Truckle Jun 05 '16 at 21:00
  • @AndrewTruckle - better to ask another question here on SO, where you'll get many more, better-qualified eyes than mine alone. Feel free to add a link to that question as a comment, here, if you want me to find it – jimbobmcgee Jun 05 '16 at 21:15
  • Just have :) http://stackoverflow.com/questions/37646892/how-do-i-draw-a-transparent-rectangle Thanks! – Andrew Truckle Jun 05 '16 at 21:21