views:

530

answers:

6

I am doing a custom control (inherited from VisualBasic.PowerPacks.LineShape), that should be painted like as standard one, but also having a Icon displayed near it.

So, I just overrided OnPaint like this:

protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
{
    e.Graphics.DrawIcon(myIcon, StartPoint.X, StartPoint.Y);
    base.OnPaint(e);
}

Now, everything is OK, but when my control moves, the icon still remains drawn on the ancient place.

Is there a way to paint it properly?

alt text Real project situation

CODE: The sample code for tests

alt text

using Microsoft.VisualBasic.PowerPacks;
using System.Windows.Forms;
using System.Drawing;

namespace LineShapeTest
{
    /// 
    /// Test Form
    /// 
    public class Form1 : Form
    {        
        IconLineShape myLine = new IconLineShape();
        ShapeContainer shapeContainer1 = new ShapeContainer();
        Panel panel1 = new Panel();

        public Form1()
        {
            this.panel1.Dock = DockStyle.Fill;
            // load your back image here
            this.panel1.BackgroundImage = 
                global::WindowsApplication22.Properties.Resources._13820t;
            this.panel1.Controls.Add(shapeContainer1);

            this.myLine.StartPoint = new Point(20, 30);
            this.myLine.EndPoint = new Point(80, 120);
            this.myLine.Parent = this.shapeContainer1;

            MouseEventHandler panelMouseMove = 
                new MouseEventHandler(this.panel1_MouseMove);
            this.panel1.MouseMove += panelMouseMove;
            this.shapeContainer1.MouseMove += panelMouseMove;

            this.Controls.Add(panel1);
        }

        private void panel1_MouseMove(object sender, MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left)
            {
                myLine.StartPoint = e.Location;
            }
        }
    }

    /// 
    /// Test LineShape
    /// 
    public class IconLineShape : LineShape
    {
        Icon myIcon = SystemIcons.Exclamation;

        protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
        {
            e.Graphics.DrawIcon(myIcon, StartPoint.X, StartPoint.Y);
            base.OnPaint(e);
        }
    }
}

Nota Bene, for the lineShape:

Parent = ShapeContainer
Parent.Parent = Panel

Update 1 TRACES

In this variant of OnPaint, we have traces:

protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = Parent.Parent.CreateGraphics();
    g.DrawIcon(myIcon, StartPoint.X, StartPoint.Y);            
    base.OnPaint(e);
}        

alt text

Update 2 BLINKS

In this variant of OnPaint, we have a blinking image:

protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
{
    Parent.Parent.Invalidate(this.Region, true);
    Graphics g = Parent.Parent.CreateGraphics();
    g.DrawIcon(myIcon, StartPoint.X, StartPoint.Y);            
    base.OnPaint(e);
}  

alt text

Update 3: External Invalidation

This variant works well, but... from exterior of IconLineShape class:

private void panel1_MouseMove(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        Region r = myLine.Region;
        myLine.StartPoint = e.Location;
        panel1.Invalidate(r);
    }
}


/// 
/// Test LineShape
/// 
public class IconLineShape : LineShape
{
    Icon myIcon = SystemIcons.Exclamation;
    Graphics parentGraphics;

    protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
    {
        parentGraphics.DrawIcon(myIcon, StartPoint.X, StartPoint.Y);
        base.OnPaint(e);
    }

    protected override void OnParentChanged(System.EventArgs e)
    {
        // Parent is a ShapeContainer
        // Parent.Parent is a Panel
        parentGraphics = Parent.Parent.CreateGraphics();
        base.OnParentChanged(e);
    }
}

Even this resolves the problem of the test example, I need this control to be done inside the control, because I can't force the external "clients" of this control do not forget to save the old region and invalidate the parent each time changing a location...

A: 

Finally, ended up to add a PictureBox instead of drawing the icon by myself.

using Microsoft.VisualBasic.PowerPacks;
using System.Windows.Forms;
using System.Drawing;

namespace LineShapeTest
{
    /// 
    /// Test Form
    /// 
    public class Form1 : Form
    {
        IconLineShape myLine = new IconLineShape();
        ShapeContainer shapeContainer1 = new ShapeContainer();
        Panel panel1 = new Panel();

        public Form1()
        {
            this.panel1.Dock = DockStyle.Fill;
            // load your back image here
            //this.panel1.BackgroundImage =
            //global::WindowsApplication22.Properties.Resources._13820t;
            this.panel1.BackColor = Color.White;
            this.panel1.Controls.Add(shapeContainer1);

            this.myLine.StartPoint = new Point(20, 30);
            this.myLine.EndPoint = new Point(80, 120);
            this.myLine.Parent = this.shapeContainer1;

            MouseEventHandler panelMouseMove =
                new MouseEventHandler(this.panel1_MouseMove);
            this.panel1.MouseMove += panelMouseMove;
            this.shapeContainer1.MouseMove += panelMouseMove;

            this.Controls.Add(panel1);
        }

        private void panel1_MouseMove(object sender, MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left)
            {
                myLine.StartPoint = e.Location;
            }
        }
    }

    /// 
    /// Test LineShape
    /// 
    public class IconLineShape : LineShape
    {
        Icon myIcon = SystemIcons.Exclamation;
        PictureBox pictureBox = new PictureBox();

        public IconLineShape()
        {
            pictureBox.Image = Bitmap.FromHicon(myIcon.Handle);
            pictureBox.Size = myIcon.Size;
            pictureBox.Visible = true;
        }

        protected override void OnMove(System.EventArgs e)
        {
            base.OnMove(e);
            pictureBox.Location = this.StartPoint;
        }

        protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
        {
            base.OnPaint(e);
            pictureBox.Invalidate();
        }

        public override bool HitTest(int x, int y)
        {            
            return base.HitTest(x, y) |
                pictureBox.RectangleToScreen(
                    pictureBox.DisplayRectangle).Contains(new Point(x, y));
        }

        protected override void OnParentChanged(System.EventArgs e)
        {
            // Parent is a ShapeContainer
            // Parent.Parent is a Panel
            pictureBox.Parent = Parent.Parent;
            base.OnParentChanged(e);
        }
    }
}

Thanks everybody for participating!

serhio
somebody gone frustrated...
serhio
You don't need the OnPaint method. Notice that the background of the PictureBox paints over your line. Remove the OnPaint and call pictureBox.Invalidate() after pictureBox.Location = this.StartPoint; This will fix the problem.
AMissico
Calling pictureBox.Invalidate(); in OnPaint is not a good idea, as henchmen points out.
AMissico
pictureBox.Image = Bitmap.FromHicon(myIcon.Handle); is not a good idea. This method is for creating bitmaps from handles returned by the Windows API. You should use SystemIcons.Exclamation.ToBitmap(); You don't need to maintain the icon. This will help save resources
AMissico
@AMissico: If I don't call pictureBox.Invalidate(), I see the line traces over the picture. This is because I call it. As for Icon, in the real project surely I don't use SystemIconsExcamation but a custom icon, that was for demonstration purpose only.
serhio
@serhio: In your sample code, you have to call Invalidate because you do not associate the PictureBox with a control collection. I am only suggesting that you call Invalidate after setting the picture box's location. This will remove the need for OnPaint and will resolve the problem of the picture box painting over the line shape.
AMissico
@AMissico: Why I don't associate pictureBox with a control collection? I do it: `pictureBox.Parent = Parent.Parent;` (this will bring pictureBox in the Panel's Controls collection). As for Invaliadting after chnaging location - maybe, if this location is not set multiple times in different places.
serhio
@serhio; Ah, I see what you are doing now.
AMissico
Have you notice a rash of down votes on accepted answers for no apparent reason?
AMissico
yeah... crazy people...
serhio
+1  A: 

Did you try clearing the buffer (draw a filling rectangle with background color)? Also make sure to reset clipping regions to the size of your control, then draw your icon and then call the parents paint.

Johannes Rudolph
could you advice how can I reset clipping regions?
serhio
A: 

did you do this on the custom control?

to remove the "stroboscope" effects

Public Sub New()
    Me.SetStyle(ControlStyles.ResizeRedraw Or _
                ControlStyles.DoubleBuffer Or _
                ControlStyles.UserPaint Or _
                ControlStyles.AllPaintingInWmPaint, _
                True)
    Me.UpdateStyles()

End Sub
Fredou
unfortunately the base class (PowerPacks.LineShape) does not have such a method (SetStyle)
serhio
+1  A: 

try to change the following functio of your form to invalidate the buffer where the icon was before and thereby to remove the rest (untestet code):

    private void panel1_MouseMove(object sender, MouseEventArgs e)
    {
        if (e.Button == MouseButtons.Left)
        {
            Point oldPos = myLine.StartPoint;
            myLine.StartPoint = e.Location;
            this.Invalidate(new Recangle(oldPos.X, oldPos.Y, myLine.Width, myLine.Height));
        }
    }

if this doesn't work, try:

    private void panel1_MouseMove(object sender, MouseEventArgs e)
    {
        if (e.Button == MouseButtons.Left)
        {
            myLine.StartPoint = e.Location;
            this.Refresh();
        }
    }

This method may not be recommended because of performace issues (the whole buffer is cleared), but of nothing else is working...

henchman
@henchman: Thanks, it worked, see my update 3. However I can't leave this logic outside the control, I need that the control itself repaints properly.
serhio
@serhio: When you paint a line, record the position in another variable. Then, the next time you paint, if the position is different, you can invalidate the old area.
John Fisher
@John: see Update 2. When I invalidate the old area, that could intersect the new one, an infinite repainting cycle begins.
serhio
@serhio: You shouldn't end up with an infinite repainting cycle at all. Even without doing work to make sure you don't invalidate the portion you're about to draw. Why? Because you would check to see if you had already drawn the line in the current position. If the line hasn't moved, and you're redrawing it, you don't need to invalidate the old position.
John Fisher
A: 

Does this help? It seems you are too caught up with the LineShape. To me, deriving from RectangleShape made more sense. I wrapped everthing up in a helper that takes care of the details. This helper is a standard technique I use for relating controls together without creating a "composite control", which is usually easier.

using Microsoft.VisualBasic.PowerPacks;
using System.Windows.Forms;
using System.Drawing;

namespace LineShapeTest {

    public partial class Form1 : Form {

        /*  Designer support through
         *  Create Panel
         *  Set panel's background image
         *  Add LineShape
         *  Add IconShape
         *  Create IconicLineShapeHelper
         */
        public Form1() {
            InitializeComponent();
            IconicLineShapeHelper arbitrary1 = new IconicLineShapeHelper(lineShape1, iconShape1);
            IconicLineShapeHelper arbitrary2 = new IconicLineShapeHelper(lineShape2, iconShape2);
        }

        private Panel panel1;
        private ShapeContainer shapeContainer1;
        private LineShape lineShape1;
        private IconShape iconShape1;
        private ShapeContainer shapeContainer2;
        private LineShape lineShape2;
        private IconShape iconShape2;

    #region [ Form1.Designer.cs ]
        private System.ComponentModel.IContainer components = null;
        protected override void Dispose(bool disposing) {
            if (disposing && (components != null)) {
                components.Dispose();
            }
            base.Dispose(disposing);
        }
        #region Windows Form Designer generated code
        private void InitializeComponent() {
            this.lineShape1 = new Microsoft.VisualBasic.PowerPacks.LineShape();
            this.panel1 = new System.Windows.Forms.Panel();
            this.shapeContainer1 = new Microsoft.VisualBasic.PowerPacks.ShapeContainer();
            this.iconShape1 = new LineShapeTest.IconShape();
            this.shapeContainer2 = new Microsoft.VisualBasic.PowerPacks.ShapeContainer();
            this.lineShape2 = new Microsoft.VisualBasic.PowerPacks.LineShape();
            this.iconShape2 = new LineShapeTest.IconShape();
            this.panel1.SuspendLayout();
            this.SuspendLayout();
            // 
            // lineShape1
            // 
            this.lineShape1.Name = "lineShape1";
            this.lineShape1.X1 = 13;
            this.lineShape1.X2 = 88;
            this.lineShape1.Y1 = 11;
            this.lineShape1.Y2 = 34;
            // 
            // panel1
            // 
            this.panel1.BackgroundImage = global::LineShapeTest.Properties.Resources._13820t;
            this.panel1.Controls.Add(this.shapeContainer1);
            this.panel1.Location = new System.Drawing.Point(27, 24);
            this.panel1.Name = "panel1";
            this.panel1.Size = new System.Drawing.Size(162, 122);
            this.panel1.TabIndex = 1;
            // 
            // shapeContainer1
            // 
            this.shapeContainer1.Location = new System.Drawing.Point(0, 0);
            this.shapeContainer1.Margin = new System.Windows.Forms.Padding(0);
            this.shapeContainer1.Name = "shapeContainer1";
            this.shapeContainer1.Shapes.AddRange(new Microsoft.VisualBasic.PowerPacks.Shape[] {
            this.iconShape1,
            this.lineShape1});
            this.shapeContainer1.Size = new System.Drawing.Size(162, 122);
            this.shapeContainer1.TabIndex = 0;
            this.shapeContainer1.TabStop = false;
            // 
            // iconShape1
            // 
            this.iconShape1.BorderStyle = System.Drawing.Drawing2D.DashStyle.Custom;
            this.iconShape1.Location = new System.Drawing.Point(88, 64);
            this.iconShape1.Name = "iconShape1";
            this.iconShape1.Size = new System.Drawing.Size(32, 32);
            // 
            // shapeContainer2
            // 
            this.shapeContainer2.Location = new System.Drawing.Point(0, 0);
            this.shapeContainer2.Margin = new System.Windows.Forms.Padding(0);
            this.shapeContainer2.Name = "shapeContainer2";
            this.shapeContainer2.Shapes.AddRange(new Microsoft.VisualBasic.PowerPacks.Shape[] {
            this.iconShape2,
            this.lineShape2});
            this.shapeContainer2.Size = new System.Drawing.Size(292, 266);
            this.shapeContainer2.TabIndex = 2;
            this.shapeContainer2.TabStop = false;
            // 
            // lineShape2
            // 
            this.lineShape2.Name = "lineShape2";
            this.lineShape2.X1 = 48;
            this.lineShape2.X2 = 123;
            this.lineShape2.Y1 = 187;
            this.lineShape2.Y2 = 210;
            // 
            // iconShape2
            // 
            this.iconShape2.Location = new System.Drawing.Point(136, 220);
            this.iconShape2.Name = "iconShape2";
            this.iconShape2.Size = new System.Drawing.Size(75, 23);
            // 
            // Form1
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(292, 266);
            this.Controls.Add(this.panel1);
            this.Controls.Add(this.shapeContainer2);
            this.Name = "Form1";
            this.Text = "Form1";
            this.panel1.ResumeLayout(false);
            this.ResumeLayout(false);

        }
        #endregion
    }
    #endregion

    public class IconicLineShapeHelper {
        ShapeContainer _container;
        LineShape _line;
        IconShape _icon;
        public IconicLineShapeHelper(LineShape line, IconShape icon) {
            _container = line.Parent;
            _line = line;
            _icon = icon;
            _container.MouseMove += new MouseEventHandler(_container_MouseMove);
        }
        void _container_MouseMove(object sender, MouseEventArgs e) {
            if (e.Button == MouseButtons.Left) {
                _line.StartPoint = e.Location;
                _icon.Location = e.Location;
            }
        }
    }
    public class IconShape : RectangleShape {
        Icon _icon = SystemIcons.Exclamation;
        public IconShape() {
            this.Size = new System.Drawing.Size(32, 32);
            this.BorderStyle = System.Drawing.Drawing2D.DashStyle.Custom;
        }
        protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) {
            e.Graphics.DrawIcon(_icon, this.Location.X, this.Location.Y);
            base.OnPaint(e);
        }
    }
}
AMissico
Thanks for idea. However, the LineShape is necessary. See my added real project screen. User can move the lines, not rectangles (here LineShape is used for hittest - clicks, mouseover - tooltip, and repainting logic - transparency)
serhio
I am still using the LineShape. I am only associating a icon with the line. The helper class only keeps the icon moving with the line. The RectangeShape knows how to properly update the background when moving. Therefore, it is a good candidate to inherit from.
AMissico
You can still use IconLineShape. Just create a IconShape and IconicLineShapeHelper in the constructor for internal use.
AMissico
I understand. Is it to note that container as ShapeContainer is not good to manage MouseMove events, because MouseCursor easily leaves the slim line area and the moving ends. I think I will just add a PictureBox, finally :) I even tested and it seems to work.
serhio
I duplicated what you had as best as possible, and trying to keep it as simple and as flexible as possible. I figured once you got something working, you could adjust to your needs.
AMissico
Why not just override HitTest and enlarge the area?
AMissico
+1  A: 

just adding another, completely different (hopefully again working :-) solution. As i wasn't aware of the requirement 'code has to be inside the class', this is the follow up.

Axiom: NEVER call Invalidate() or Refresh() in OnPaint or OnPaintBackground, because you will (always) end up in a infinite loop.

So we have to find another place for them. I tried to compile your classes within Visual Studio, but i couldn't find the class LineShape (Microsoft.VisualBasic.PowerPacks.Vs.dll didn't do the trick), so again untestet code.

what did i do?

  1. Removed the MouseMove handler from Form and put it inside the IconLineShape class. Shall be okay, because if customer wants drag and drop, just fine. Try one of the described solutions.
  2. Added a property to disable drag and drop in IconLineShape (if customer is not fine with that :-). without drag and drop, we would not have the problem in the first place.

-

    public class Form1 : Form
    {
       IconLineShape myLine = new IconLineShape();
       ShapeContainer shapeContainer1 = new ShapeContainer();
       Panel panel1 = new Panel();

       public Form1()
       {
           this.panel1.Dock = DockStyle.Fill;
           // load your back image here
           this.panel1.BackgroundImage =
               global::WindowsApplication22.Properties.Resources._13820t;
           this.panel1.Controls.Add(shapeContainer1);

           this.myLine.StartPoint = new Point(20, 30);
           this.myLine.EndPoint = new Point(80, 120);
           this.myLine.Parent = this.shapeContainer1;

           this.Controls.Add(panel1);
       }
   }

   public class IconLineShape : LineShape
   {
       Icon myIcon = SystemIcons.Exclamation;

       protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
       {
           e.Graphics.DrawIcon(myIcon, StartPoint.X, StartPoint.Y);
           base.OnPaint(e);
       }

       protected override void OnMouseMove(MouseEventArgs e)
       {
           if (draggable && 
               e.Button == MouseButtons.Left &&
               !this.StartPoint.Equals(e.Location))
           {
               Region r = this.Region.Clone();

               this.StartPoint = e.Location;

               // try solution 1
               this.Invalidate(r);

               // solution 2; walk up to the upmost parent and refresh
               // as said before, this.Invalidate() is to be preferred
               Control currentParent = this.Parent;
               while (currentParent.Parent != null)
               {
                   currentParent = currentParent.Parent;
               }
               currentParent.Refresh();
           } 
       }

       private bool draggable = true;

       public bool Draggable
       {
           get { return this.draggable; }
           set { this.draggable = value; }
       }
   }

Please give feedback.

henchman
MS PowerPacks (2.7 MB) http://msdn.microsoft.com/en-us/vbasic/bb735936.aspx are not included in .NET framework, so needs to be installed separately.The MouseMove will not work on the lineShape, cause is very slim, and when user quickly moves the mouse it loses the line, and mousemove event ends.
serhio
that would be strange. there can be no difference whether you overwrite the onmousemove-method or assign a mousemove event handler 'from the outside', as this event handler only gets called as onmousemove does. have you testet it?
henchman
ShapeContainer detects mouse movements only when the mouse is over the contained shapes (one line in our case). So, to move the line using only the ShapeContainer mouseMove we should move the mouse pointer very slowly - without leaving the line area.
serhio