Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Allow custom control to correctly scroll out of view

Tags:

c#

winforms

I have a custom control that consists of a filled rounded rectangle with text. (The actual control is more complicated but the code shown here has the sames symptoms.) I attached instances of the control to a Panel, and made that Panel a child of another Panel with AutoScroll = true. I thought that would be enough for correct scroll behavior, but if you scroll such that the left side of my control should go off the left side of the panel it sticks and the control shrinks. Same with scrolling such that the control should go off at the top. (Bottom and right seem to not be a problem.) Here is the sample code (requires a reference to System.Windows.Forms and System.Drawing.) I am using Visual Studio 2010 with .NET 4 client on Windows.

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


namespace CustomControlScrollTest
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }

    // Form with a Dock = Fill panel with autoscroll turned on, and a nested panel
    // with few custom roundRectControls.
    public class Form1 : Form
    {
        Panel autoScrollPanel;
        Panel rectPanel;

        public Form1()
        {
            Size = new Size(300, 200);

            autoScrollPanel = new Panel();
            autoScrollPanel.Dock = DockStyle.Fill;
            autoScrollPanel.AutoScroll = true;
            autoScrollPanel.AutoScrollMinSize = new Size(600, 450);

            autoScrollPanel.Resize += autoScrollPanel_Resize;
            autoScrollPanel.Scroll += autoScrollPanel_Scroll;

            Controls.Add(autoScrollPanel);

            rectPanel = new Panel();
            rectPanel.Size = autoScrollPanel.AutoScrollMinSize;
            rectPanel.Controls.AddRange(new RoundRectControl[] {
                new RoundRectControl(),
                new RoundRectControl(),
                new RoundRectControl(),
                new RoundRectControl(),
                new RoundRectControl()
            });

            foreach (Control c in rectPanel.Controls)
            {
                c.Click += c_Click;
            }

            autoScrollPanel.Controls.Add(rectPanel);

            placeBoxes();
        }

        // we want to be able to recalculate the boxes position at any time
        // in the real program this occurs due to model changes
        void c_Click(object sender, EventArgs e)
        {
            placeBoxes();
        }

        void autoScrollPanel_Scroll(object sender, ScrollEventArgs e)
        {
            Refresh();
        }

        void autoScrollPanel_Resize(object sender, EventArgs e)
        {
            Refresh();
        }

        private void placeBoxes()
        {
            for (int i = 0; i < rectPanel.Controls.Count; ++i)
            {
                int j = i + 1;
                var node = rectPanel.Controls[i] as RoundRectControl;
                if (node != null)
                {
                    node.Title = "Hello (" + j + ")";
                    node.Location = new Point(i * 100, j * 75);
                    node.Visible = true;                   
                }
            }
        }
    }

    // A rounded rectangle filled blue with a black border and white text
    // the size is determined by the text
    public class RoundRectControl : Control
    {
        public RoundRectControl()
        {
            var f = SystemFonts.MessageBoxFont;
            titleFont = new Font(f.Name, f.SizeInPoints + 2, FontStyle.Bold, GraphicsUnit.Point);
            ResizeRedraw = true;
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            var g = e.Graphics;

            var left = e.ClipRectangle.X;
            var right = left + titleWidth + 2 * radius;
            var top = e.ClipRectangle.Y;
            var bottom = top + nodeHeight + 2 * radius;

            var r2 = 2 * radius;

            using (var path = new GraphicsPath())
            {
                path.AddArc(left,       bottom - r2, r2,  r2, 90,  90);
                path.AddArc(left,       top,         r2,  r2, 180, 90);
                path.AddArc(right - r2, top,         r2,  r2, 270, 90);
                path.AddArc(right - r2, bottom - r2, r2,  r2, 0,   90);
                path.CloseFigure();

                g.FillPath(titleBrush, path);
                g.DrawPath(borderPen, path);
            }

            g.DrawString(title, titleFont, titleTextBrush, left + radius, top + radius);
        }

        private string title;
        public string Title
        {
            get { return title; }
            set
            {
                title = value;
                Size = getSize();
                Invalidate();
            }
        }

        private Brush titleBrush = Brushes.Blue;

        private Brush titleTextBrush = Brushes.White;

        private Pen borderPen = Pens.Black;

        private Size getSize()
        {
            var g = CreateGraphics();
            var titleSize = g.MeasureString(title, titleFont);
            titleWidth = (int)titleSize.Width;
            nodeHeight = (int)titleSize.Height;
            return new Size(titleWidth + 2 * radius + 1, nodeHeight + 2 * radius + 1);
        }

        public override Size GetPreferredSize(Size proposedSize)
        {
            return getSize();
        }

        private int titleWidth;
        private int nodeHeight;

        private Font titleFont;

        private int radius = 5;
    }
}
like image 658
Jeremy Sorensen Avatar asked Sep 22 '14 22:09

Jeremy Sorensen


1 Answers

Try this

protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);
        var g = e.Graphics;

        //total width and height of rounded rectangle
        var width = titleWidth + 2 * radius + 1;
        var height = nodeHeight + 2 * radius + 1;

        var left = e.ClipRectangle.X;
        var top = e.ClipRectangle.Y;

        //check if clipping occurs. If yes, set to 0
        if (width > e.ClipRectangle.Width)
        {
            left = 0; // *= -1;
        }

        //check if clipping occurs.If yes, set to 0
        if (height > e.ClipRectangle.Height)
        {
            top = 0; // *= -1
        }

        var right = left + titleWidth + 2 * radius;
        var bottom = top + nodeHeight + 2 * radius;

        var r2 = 2 * radius;

        using (var path = new GraphicsPath())
        {
            path.AddArc(left, bottom - r2, r2, r2, 90, 90);
            path.AddArc(left, top, r2, r2, 180, 90);
            path.AddArc(right - r2, top, r2, r2, 270, 90);
            path.AddArc(right - r2, bottom - r2, r2, r2, 0, 90);
            path.CloseFigure();

            g.FillPath(titleBrush, path);
            g.DrawPath(borderPen, path);
        }

        g.DrawString(title, titleFont, titleTextBrush, left + radius, top + radius);
    }

The problem is when you are scrolling to the right(rects moving left, e.ClipRectangle.Width becomes smaller) and the rectangle goes out of area, the e.ClipRectangle.X is positive! So in this case we set it to zero. e.ClipRectangle.X can not be negative, so even if you invert it(solution before edit) it becomes zero.

EDIT

You can simply do

var left = 0;
var right = left + titleWidth + 2 * radius;
var top = 0;
var bottom = top + nodeHeight + 2 * radius;

Both are working

You can use these styles

this.SetStyle(ControlStyles.DoubleBuffer, true);
this.SetStyle(ControlStyles.UserPaint, true);
this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);

to avoid flickering

valter