通过扩展程序在 Visual Studio Designer 中的 windows.forms.controls 上绘制装饰

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

我写了一个 Visual Studio 2013 扩展,观察 Windows.Forms 设计师 windows。当开发人员在设计器 window 中更改控件时,扩展会尝试验证结果是否与我们的 ui 样式 guidelines 一致。如果发现可能的违规行为,它们将列在工具 window 中。这一切都很好。 但现在我想在设计器中标记不一致的控件window,例如用红框或类似的东西。

不幸的是,我没有找到在设计器中的控件上绘制装饰的方法window。我知道如果您开发自己的 ControlDesigner,您可以绘制这些装饰,但我需要从 "outside" 控件的设计器中完成。我只有 Dte2.ActiveWindow 中的 IDesignerHost,并且可以通过该主机访问控件和 ControlDesigner。我找不到任何方法来从 "outside" ControlDesigners 添加装饰。 我现在的解决方法是捕捉控件的 Paint-Events 并尝试从那里绘制我的装饰品。这不适用于所有控件(即组合框等),因为并非所有控件都允许您在其上绘制。所以我不得不使用他们的 parent 控件的 Paint 事件。此解决方案还有其他缺点。

希望有人能告诉我有没有更好的方法。我很确定必须有一个:如果您使用 Menu->View->Tab Order(不确定正确的英文菜单标题,我使用的是德语 IDE),您可以看到IDE 本身能够装饰控件(没有屏幕截图,因为这是我在 SO 上的第一个 post),而且我确信它没有像我一样使用变通方法。它是如何做到的?

我已经用谷歌搜索了几个星期了。感谢您提供任何帮助、建议、研究起点....

更新:

也许这张截图会更清楚一点:

那些蓝色编号的插入符是 Visual Studio 从“视图”菜单中选择 Tab 键顺序时显示的内容。我的问题是 IDE.

是如何完成的

如前所述,我尝试在控件的 Paint 事件中执行此操作,但是例如ComboBox 实际上不支持该事件。如果我使用 parent 的 Paint 事件,我只能绘制 "around" child 控件,因为它们是在 parent.[=21= 之后绘制的]

我也考虑过在控件或 ControlDesigner 上使用反射,但我不确定如何挂钩受保护的 OnPaintAdornments 方法。而且我不认为 IDE 开发人员使用了那些 "dirty" 技巧。

使用System.Drawing.Graphics.FromHwnd方法,为设计者传入HWNDwindow。

通过 pinvoke. Perhaps use tools like Inspect to find window classes 深入了解 visual studio 的 window 句柄,获取 HWND 以及可能帮助您识别正确(设计者)window 的其他信息.

我编写了一个 C# 程序来帮助您入门 here

我相信你正在寻找 BehaviorService architecture. The architecture with supporting parts like Behavior, Adorner and Glyph and some examples is explained here Behavior Service Overview。例如

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.

希望对您有所帮助。

我终于有时间实施我的解决方案并希望展示它的完整性。
当然,我减少了代码以仅显示相关部分。

1。获取行为服务

这也是我不喜欢service locator (anti) pattern的原因之一。尽管阅读了很多文章,但我并没有想到我可以从我的 IDesignerHost.

中获得 BehaviorService

我现在有这样的数据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);
    }
}

所以我从 IDesignerHostRootComponent 中获取 BehaviorService 并向其添加一个新的 System.Windows.Forms.Design.Behavior.Adorner。然后我可以使用我的 AddIssueRemoveIssue 方法将我的字形添加和修改到 Adorner.

2。我的字形实现

这里是MyGlyph的实现,一个class继承自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);
    }
}

可以在已接受答案中张贴的链接中研究覆盖的详细信息。
我在控件周围(但在内部)绘制了一个红色边框,并添加了一个包含已发现问题数量的小矩形。
需要注意的一件事是我检查 Control.Visible 是否为 true。因此,当控件位于 - 例如 - 当前未选中的 TabPage 上时,我可以避免绘制装饰。

3。我的行为实现

由于 Glyph 基础 class 的构造函数需要一个从 Behavior 继承的 class 的实例,我需要创建一个新的 class .这可以留空,但我用它来在鼠标进入显示问题数量的矩形时显示工具提示:

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);
    }
}

当鼠标 enters/leaves 由 MyGlyph 实现返回 Bounds 时调用覆盖。

4。结果

最后我展示了示例结果的屏幕截图。由于这是由实际实现完成的,因此工具提示更高级一些。该按钮未与所有组合框对齐,因为它有点太左了:

再次感谢 Ivan Stoev 为我指出正确的解决方案。我希望我能说清楚我是如何实现的。