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

问题描述 投票:18回答:3

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

不幸的是,我没有找到在设计器窗口中绘制控件装饰的方法。我知道如果你开发自己的ControlDesigner,你可以绘制那些装饰品,但我需要从控制设计师的“外部”进行。我只有来自IDesignerHostDte2.ActiveWindow,可以通过该主机访问Controls和ControlDesigners。我找不到任何方法来从ControlDesigners的“外部”添加装饰品。我现在的解决方法是捕获控件的Paint-Events并尝试从那里绘制我的装饰品。这对所有控件(即ComboBox等)都不适用,因为并非所有控件都可以使用它们。所以我不得不使用他们的父控件的Paint事件。此解决方案还有其他缺点。

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

我几周来一直在谷歌上搜索。感谢您的帮助,建议,研究起点......

更新:

也许这个截图有点清晰:

Screenshot tab order

从View菜单中选择Tab顺序时,Visual Studio显示的是蓝色编号的插入符号。我的问题是如何通过IDE完成这项工作。

如上所述,我试图在控件的Paint事件中执行此操作,但是ComboBox实际上并不支持这一事件。如果我使用父级的Paint事件,我只能在子控件周围绘制,因为它们是在父级之后绘制的。

我还想过在控件或ControlDesigners上使用反射,但我不知道如何挂钩受保护的OnPaintAdornments方法。我不认为IDE开发人员使用那些“肮脏”的技巧。

c# wpf visual-studio windows-forms-designer vsix
3个回答
11
投票

我相信你正在寻找BehaviorService建筑。 Behavior解释了AdornerGlyphBehavior Service Overview等支持部分的架构和一些例子。例如

扩展设计时用户界面

BehaviorService模型使新功能可以轻松地在现有设计器用户界面上分层。新UI保持独立于其他先前定义的Glyph和Behavior对象。例如,某些控件上的智能标记由控件右上角的Glyph访问(智能标记字形)。

智能标记代码创建自己的Adorner图层,并将Glyph对象添加到此图层。这使智能标记Glyph对象与选择Glyph对象分开。将新的Adorner添加到Adorners集合中的必要代码非常简单。

等等

希望有所帮助。


8
投票

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

1. Obtaining the BehaviorService

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

我现在有类似这样的数据类:

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

所以我从BehaviorServiceRootComponent获得IDesignerHost并添加一个新的System.Windows.Forms.Design.Behavior.Adorner。然后我可以使用我的AddIssueRemoveIssue方法添加和修改我的字形到Adorner

2. My Glyph implementation

这是MyGlyph的实现,这是一个继承自`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. My Behavior implementation

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

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

当鼠标进入/离开Bounds实现返回的MyGlyph时,将调用覆盖。

4. Results

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

enter image description here

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


2
投票

使用System.Drawing.Graphics.FromHwnd方法,为设计器窗口传入HWND。

通过pinvoke钻进视觉工作室的窗口把手,获得HWND。也许使用像Inspect这样的工具来查找窗口classes以及其他可能帮助您识别正确(设计者)窗口的信息。

我写了一个C#程序,让你开始使用here

© www.soinside.com 2019 - 2024. All rights reserved.