Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Draw adornments on windows.forms.controls in Visual Studio Designer from an extension

I wrote an Visual Studio 2013 extension that observes Windows.Forms designer windows. When a developer is changing controls in the designer window, the extension tries to verify that the result is consistent with our ui style guidelines. If possible violations are found they are listed in a tool window. This all works fine. But now I would like to mark the inconsistent controls in the designer window, for example with a red frame or something like this.

Unfortunately, I did not find a way to draw adornments on controls in a designer window. I know that you can draw those adornments if you develop your own ControlDesigner, but I need to do it from "outside" the control's designer. I only have the IDesignerHost from the Dte2.ActiveWindow and can access the Controls and ControlDesigners via that host. I could not find any way to add adornments from "outside" the ControlDesigners. My workaround for now is to catch the Paint-Events of the controls and try to draw my adornments from there. This doesn't work well for all controls (i.e. ComboBoxes etc), because not all controls let you draw on them. So I had to use their parent control's Paint event. And there are other drawbacks to this solution.

I hope someone can tell me if there is a better way. I'm pretty sure that there has to be one: If you use Menu->View->Tab Order (not sure about the correct english menu title, I'm using a german IDE), you can see that the IDE itself is able to adorn controls (no screenshot because it's my first post on SO), and I'm sure it is not using a work around like me. How does it do that?

I've been googling that for weeks now. Thanks for any help, advice, research starting points....

UPDATE:

Maybe it gets a little bit clearer with this screenshot:

Screenshot tab order

Those blue numbered carets is what Visual Studio shows when selecting Tab order from the View menu. And my question is how this is done by the IDE.

As mentioned I tried to do it in the Paint event of the controls, but e.g. ComboBox doesn't actually support that event. And if I use the parent's Paint event I can only draw "around" the child controls because they are painted after the parent.

I also thought about using reflection on the controls or the ControlDesigners, but am not sure how to hook on the protected OnPaintAdornments method. And I don't think the IDE developers used those "dirty" tricks.

like image 942
René Vogt Avatar asked Nov 05 '15 10:11

René Vogt


People also ask

How do you draw shapes in Windows Forms?

You can draw filled shapes by selecting which shape you want, click picturebox and drag mouse to get which size you want. But THIS can happens when I drag.


2 Answers

I believe you are seeking for BehaviorService architecture. The architecture with supporting parts like Behavior, Adorner and Glyph and some examples is explained here Behavior Service Overview. For instance

Extending the Design-Time User Interface

The BehaviorService model enables new functionality to be easily layered on an existing designer user interface. New UI remains independent of other previously defined Glyph and Behavior objects. For example, the smart tags on some controls are accessed by a Glyph in the upper-right-hand corner of the control (Smart Tag Glyph).

The smart tag code creates its own Adorner layer and adds Glyph objects to this layer. This keeps the smart tag Glyph objects separate from the selection Glyph objects. The necessary code for adding a new Adorner to the Adorners collection is straightforward.

etc.

Hope that helps.

like image 126
Ivan Stoev Avatar answered Sep 21 '22 07:09

Ivan Stoev


I finally had the time to implement my solution and want to show it for completeness.
Of course I reduced the code to show only the relevant parts.

1. Obtaining the BehaviorService

This is one of the reasons why I don't like the service locator (anti) pattern. Though reading a lot of articles, I didn't came to my mind that I can obtain a BehaviorService from my IDesignerHost.

I now have something like this data class:

public class DesignerIssuesModel
{
    private readonly BehaviorService m_BehaviorService;
    private readonly Adorner m_Adorner = new Adorner();
    private readonly Dictionary<Control, MyGlyph> m_Glyphs = new Dictionary<Control, MyGlyph>();

    public IDesignerHost DesignerHost { get; private set; }

    public DesignerIssuesModel(IDesignerHost designerHost)
    {
        DesignerHost = designerHost;
        m_BehaviorService = (BehaviorService)DesignerHost.RootComponent.Site.GetService(typeof(BehaviorService));
        m_BehaviorService.Adornders.Add(m_Adorner);
    }

    public void AddIssue(Control control)
    {
        if (!m_Glyphs.ContainsKey(control))
        {
            MyGlyph g = new MyGlyph(m_BehaviorService, control);
            m_Glyphs[control] = g;
            m_Adorner.Glyphs.Add(g);
        }

        m_Glyphs[control].Issues += 1; 
    }
    public void RemoveIssue(Control control)
    {
        if (!m_Glyphs.ContainsKey(control)) return;
        MyGlyph g = m_Glyphs[control];
        g.Issues -= 1;
        if (g.Issues > 0) return;
        m_Glyphs.Remove(control);
        m_Adorner.Glyphs.Remove(g);
    }
}

So I obtain the BehaviorService from the RootComponent of the IDesignerHost and add a new System.Windows.Forms.Design.Behavior.Adorner to it. Then I can use my AddIssue and RemoveIssue methods to add and modify my glyphs to the Adorner.

2. My Glyph implementation

Here is the implementation of MyGlyph, a class inherited from System.Windows.Forms.Design.Behavior.Glyph:

public class MyGlyph : Glyph
{
    private readonly BehaviorService m_BehaviorService;
    private readonly Control m_Control;

    public int Issues { get; set; }
    public Control Control { get { return m_Control; } }

    public VolkerIssueGlyph(BehaviorService behaviorService, Control control) : base(new MyBehavior())
    {
        m_Control = control;
        m_BehaviorService = behaviorService;            
    }

    public override Rectangle Bounds
    {
        get
        {
            Point p = m_BehaviorService.ControlToAdornerWindow(m_Control);
            Graphics g = Graphics.FromHwnd(m_Control.Handle);
            SizeF size = g.MeasureString(Issues.ToString(), m_Font);
            return new Rectangle(p.X + 1, p.Y + m_Control.Height - (int)size.Height - 2, (int)size.Width + 1, (int)size.Height + 1);
        }
    }
    public override Cursor GetHitTest(Point p)
    {
        return m_Control.Visible && Bounds.Contains(p) ? Cursors.Cross : null;
    }
    public override void Paint(PaintEventArgs pe)
    {
        if (!m_Control.Visible) return;
        Point topLeft = m_BehaviorService.ControlToAdornerWindow(m_Control);
        using (Pen pen = new Pen(Color.Red, 2))
            pe.Graphics.DrawRectangle(pen, topLeft.X, topLeft.Y, m_Control.Width, m_Control.Height);

        Rectangle bounds = Bounds;
        pe.Graphics.FillRectangle(Brushes.Red, bounds);
        pe.Graphics.DrawString(Issues.ToString(), m_Font, Brushes.Black, bounds);
    }
}

The details of the overrides can be studied in the links posted in the accepted answer.
I draw a red border around (but inside) the control and add a little rectangle containing the number of found issues.
One thing to note is that I check if Control.Visible is true. So I can avoid to draw the adornment when the control is - for example - on a TabPage that is currently not selected.

3. My Behavior implementation

Since the constructor of the Glyph base class needs an instance of a class inherited from Behavior, I needed to create a new class. This can be left empty, but I used it to show a tooltip when the mouse enters the rectangle showing the number of issues:

public class MyBehavior : Behavior
{
    private static readonly ToolTip ToolTip = new ToolTip
    {
        ToolTipTitle = "UI guide line issues found",
        ToolTipIcon = ToolTipIcon.Warning
    };
    public override bool OnMouseEnter(Glyph g)
    {
        MyGlyph glyph = (MyGlyph)g;
        if (!glyph.Control.Visible) return false;

        lock(ToolTip)
            ToolTip.Show(GetText(glyph), glyph.Control, glyph.Control.PointToClient(Control.MousePosition), 2000);
        return true;
    }
    public override bool OnMouseLeave(Glyph g)
    {
        lock (ToolTip)
            ToolTip.Hide(((MyGlyph)g).Control);
        return true;
    }
    private static string GetText(MyGlyph glyph)
    {
        return string.Format("{0} has {1} conflicts!", glyph.Control.Name, glyph.Issues);
    }
}

The overrides are called when the mouse enters/leaves the Bounds returned by the MyGlyph implementation.

4. Results

Finally I show screenshot of a example result. Since this was done by the real implementation, the tooltip is a little more advanced. The button is misaligned to all the comboboxes, because it's a little too left:

enter image description here

Thanks again to Ivan Stoev for pointing me to the right solution. I hope I could make clear how I implemented it.

like image 35
René Vogt Avatar answered Sep 22 '22 07:09

René Vogt