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

3

18

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.

Varioloid answered 5/11, 2015 at 10:50 Comment(2)
Really? Nobody? Not even a comment on clarity or a question for more details? Is something wrong with my question?Tophet
Your question is a good one. Having no comments is actually a good sign, it means your question is clear. But the topic at hand is tricky... that's the problem. Let's face it: VS plugin development is a pain, so few people will be able to help. I hope you'll get an answer, the bounty should help with that.Sillimanite
A
11

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.

Adman answered 19/12, 2015 at 21:7 Comment(3)
Unfortunately this is not what I'm seeking. Again: I develop an Visual Studio extension, not a control or control designer. The controls and control designers are already there as the developer using my extension places them in his designer window. I can't exchange them with own ones. Thanks for your effort anyway.Tophet
@RenéVogt I understand that from the beginning. But as you said, you have access to the designer host, thus you can obtain BehaviorService, add your own adorners that according to the link "will receive hit test and paint messages from the BehaviorService."Adman
Thank you very much. This really is what I wanted to do. When I finished implementing my solution (after the holidays) I will probably post details of how I've done it to leave a detailed answer to the question.Tophet
V
8

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.

Varioloid answered 3/1, 2016 at 19:33 Comment(1)
Hey, I see you made it! Just to note that you don't need DesignerHost.RootComponent.Site' to GetService, and in fact getting a service from the IDesignerHost` is logical if you look at the interface definition: public interface IDesignerHost : IServiceContainer, IServiceProvider. As you can see, you can request services as well as add/remove/replace services. Anyway, great job! +1Adman
M
2

Use the System.Drawing.Graphics.FromHwnd method, passing in the HWND for the designer window.

Get the HWND by drilling down into the window handles for visual studio, via pinvoke. Perhaps use tools like Inspect to find window classes and other information that might help you identify the correct (designer) window.

I've written a C# program to get you started here.

screenshot

Messiah answered 18/12, 2015 at 23:25 Comment(2)
Thank you for the input. I hope to find the time to try it in the next two days.Tophet
I just read your code. Sorry, but that is not what I need. Maybe my question did not state it clear: It's a Visual Studio extension. So I already have access to the controls as well as to the ControlDesigner instances. Like you I can paint over them once, but it should be a stable drawing (scrolling, scaling, repainting etc). The problem is that some controls don't raise their Paint events for you or they execute their paint code after yours. But I will experiment now with unmanaged windows api, maybe there's a way. Thank you very much for your effort!Tophet

© 2022 - 2024 — McMap. All rights reserved.