【问题标题】:How can I achieve sprite composition that actually works?我怎样才能实现真正有效的精灵组合?
【发布时间】:2015-02-21 18:07:50
【问题描述】:

我的要求:

  • 具有背景的控件可以在其上绘制精灵。
  • 需要能够以编程方式和设置拖动事件来移动精灵。
  • 精灵的图像可能具有 alpha 透明度;精灵必须与背景以及彼此正确地进行 alpha 混合。
  • 绘制顺序必须与精灵的逻辑顺序相匹配 - 单击显示在顶部的精灵应该开始拖动该精灵,而不是出现在其下方的精灵。

尝试 1

显而易见的方法是创建一个自定义控件来表示 Sprite。让我们试试吧:

public partial class Sprite : Control
{
    public Sprite()
    {
        InitializeComponent();

出于测试目的,我们将把它设置为点击 Sprite 只会将其带到前面,而无需拖动:

        this.Click += Sprite_Click;
    }

    void Sprite_Click(object sender, EventArgs e)
    {
        this.BringToFront();
    }

所以我们可以识别每个精灵并验证绘制顺序,让我们为每个精灵设置一个“框架”颜色:

    public Color FrameColor { get; set; }

然后将精灵绘制为带有纯色框架的半透明白色主体 - 然后我们应该能够验证背景是否在单个空间后面出现阴影,在精灵重叠的地方阴影更强烈,并且边框按预期重叠:

    protected override void OnPaint(PaintEventArgs pe)
    {
        Graphics g = pe.Graphics;
        g.FillRectangle(
            new SolidBrush(Color.FromArgb(128, 255, 255, 255)),
            DisplayRectangle
        );
        g.DrawRectangle(new Pen(FrameColor, 10), DisplayRectangle);
    }

然后我们可以设计一个深色背景的表格,在上面设置几个Sprite,让它们重叠,给它们不同的框架颜色,然后测试。

当然,它不起作用。默认情况下,控件具有背景颜色,看起来默认为白色或接近它的颜色。所以这些精灵正确重叠,但它们有不透明的白色中间。

尝试 2

好吧,我们当然可以将背景颜色设置为透明吗?微软文档中的一些谷歌搜索告诉我们,我们当然可以,但不能直接(as someone else on SO found out the hard way)。它需要一些配置:

        // in constructor
        SetStyle(ControlStyles.SupportsTransparentBackColor, true);
        this.BackColor = Color.Transparent;

好的,现在背景通过每个 Sprite 显示,但 Sprite 边框不显示彼此。是什么赋予了?再多谷歌搜索告诉我们,这种“透明背景颜色”支持实际上有点小技巧。基本上,父控件实现其背景绘制以从父组件复制图像数据并与之合成,忽略其他所有内容。

老实说,我希望系统的设计能够自动运行 - 即,任何时间任何绘图发生,它与下面的任何东西合成它在屏幕上,并且控件不经常相互显示的唯一原因是因为它们明显不透明的背景。但没有这样的运气。我猜整个系统是在人们出于性能原因默认不想要那种东西的时候重新设计的。

无论如何。

尝试 3

好吧,如果背景画是导致问题的原因,也许我们可以完全禁用它?

    protected override void OnPaintBackground(PaintEventArgs ignored) { }

没有。如果我们单击精灵,我们可以看到它们重新排序并相互合成 - 但它们不与背景图像合成。而且,更奇怪的是,当父窗口无效时(通过调整窗体大小,或最小化并恢复它),精灵再次变为不透明。

尝试 4

在进一步谷歌搜索之后,我们找到了 StackOverflow 的答案,如 thisthis,文章如 this 等。它们都指向同一个低级黑客:

    protected override CreateParams CreateParams
    {
        get
        {
            CreateParams cp = base.CreateParams;
            cp.ExStyle = cp.ExStyle | 0x20; 
            return cp;
        }
    }

为此,我们需要禁用绘制背景,但当然,如果我们设置“透明”背景不再重要,因为该绘制逻辑已被抑制。 (奇怪的是,也可以通过在ControlStyles 中设置Opaque 选项来禁止绘制背景;虽然这听起来与我们想要的相反,但它似乎也可以。)

嗯。它几乎有效。 Sprites 与自身和背景合成。但是现在发生了一件非常奇怪的事情:绘制顺序错误,甚至不一致。以不同的方式使窗口无效(调整大小与最小化和恢复)将在视觉上将不同的 Sprite 带到前面;但一般来说,它与点击的内容和顺序不对应。如果我们添加显式失效逻辑:

    // in click event handler
    Parent.Invalidate(this.Bounds, true);

然后单击一个精灵实际上看起来将它发送到后面视觉上 - 虽然同样,如果我们调整窗口大小或最小化并恢复它,绘制顺序可能会改变。


什么给了?我怎样才能一劳永逸地解决这个问题?我考虑过的选项:

  • 将控件烧毁;使父控件跟踪精灵列表Images,跟踪鼠标拖动和单击事件,并自行执行所有拾取和渲染逻辑。应该保证工作,但工作量太可笑了。

  • 通过覆盖背景和前景绘画,使控件根本不绘制,并让它们公开具有所需图像的属性。让父级处理所有渲染,但依靠内置的控制逻辑来进行拾取、鼠标拖动等。工作量更少,但似乎有问题(也许有一些隐藏的东西仍然迫使控件绘制一些东西?),是一个相当紧密耦合的设计,如果采用幼稚的方法来失效(?),可能仍然存在性能问题。

【问题讨论】:

  • 我从来没有设法让这个设置(控制合成超过控制)工作。根据我的经验(不一定是最好的方法),只保留所有图像并渲染到位图(最后将位图渲染到屏幕)是最简单的方法。即使有所有额外的鼠标逻辑等,我也花了几天的时间才大致了解了您所描述的内容,并且性能足够。
  • winforms 不支持透明度,或任何有用的东西。改用适当的基于 XAML 的技术,例如 WPF。
  • 如果你真的想要,你可以修复你的 CreateParams 方法;如果没有看到您的实际代码,很难确定,但可能您只是没有控制何时绘制事物,因此结果不一致。也就是说,1 对 1 的 sprite-to-control 关系可能不是你想要的。 Forms 控件是一个完整的窗口,对于一个好的基于 sprite 的系统来说实在是太重了。正如 xxbbcc 建议的那样,您应该在托管它们的单个控件中自己管理精灵。

标签: c# winforms


【解决方案1】:

经过更多研究,我似乎有了答案。


首先,关于绘制顺序的一些解释:据我所知,它在尝试4中是一致的,但是通过调整窗口大小来失效会导致不同区域逐渐失效,从而产生误导性结果.该顺序始终是从上到下的,这与您想要的合成相反,但是当您通过剪切无效区域将所有内容视为不透明时,它可以启用优化。 (鉴于剪切路径可能会变得非常复杂,我不确定这确实节省了多少 CPU 工作量,但我想它也有助于减少不使用双缓冲时的闪烁,因为您避免在任何给定的刷新。)

但是碰巧的是,如果您在两行之间阅读,discover that 在“扩展窗口样式”中有 another 标志(我们在尝试 4 中设置的 0x20 标志是 @987654325 @),称为WS_EX_COMPOSITED (value 0x02000000),它将绘制顺序反转为我们想要的。或者您可以首先更仔细地阅读文档。哎呀。

(你会认为他们可以设计它来自动处理所有这些逻辑 - 检测哪些精灵具有半透明或透明背景颜色,首先从上到下绘制具有不透明背景的精灵以加快速度,然后是其他精灵在裁剪不透明的部分时自下而上确保正确性。哦,好吧。)

这里有一个快速说明:因为合成自然涉及多次重新绘制同一区域,所以这个标志还设置了双缓冲,适用于使用该标志创建的窗口的所有子窗口(据我所知,在 Windows API用语来说,每个控件都是一个“窗口”,表单本身也是如此)。所以看起来people often use it to avoid flickering on forms with lots of controls,即使他们不打算让控件重叠。然而,具有讽刺意味的是,WS_EX_COMPOSITED 实际上可能导致 huge amounts of flickering 具有某些控件,例如TabPage。 (我正在运行 8.1 并且能够非常清楚地重现所描述的问题。)文档状态

如果窗口具有 CS_OWNDC 或 CS_CLASSDC 的类样式,则不能使用此选项。

所以也许这与它有关。我们可以通过使用诸如Panel 之类的简单控件将“合成区域”限制为区域来避免这种情况。至少在我的测试中,只要TabPage 不是Panel 的子代,即使它重叠,它也能解决问题。

似乎这在 XP 下可能不起作用,但是嘿,现在是 2014 年。


不管怎样,开始代码吧。

我们需要做的是在 Sprite 的某些祖先中设置一个CreateWindowEx 标志,使用相同的CreateParams 技术,例如它们所在的窗体:

protected override CreateParams CreateParams
{
    get
    {
        CreateParams cp = base.CreateParams;
        cp.ExStyle = cp.ExStyle | 0x02000000;
        return cp;
    }
}

我们仍然需要WS_EX_TRANSPARENT 设置,并在尝试 3 和 4 时禁用 Sprite 控件上的背景绘制。我们不需要设置一个透明的BackColor,因为我们不会绘制BackColor。我不知道为什么允许 完全透明 BackColor 被绘制会搞砸,但显然确实如此。

即使有干预控件,这显然也可以工作,即如果我们将Sprite 控件放在Panel 上的Form 内。 (如果我们想将效果限制为Panel,如上所述,我们必须继承Panel;但我的项目已经这样做了:))在我的测试项目中一切正常,我什至不需要明确地Invalidate 任何东西。

【讨论】:

  • 有趣。当我解决这个问题时,我只是在底层图像的 PictureBox 内将每个图像制作为整个画布大小的 PictureBox,并在其中简单地绘制全尺寸图像,并在正确的位置绘制实际图像。这也奏效了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-11-24
  • 1970-01-01
  • 2017-02-02
  • 2021-10-31
  • 1970-01-01
  • 2021-07-10
  • 1970-01-01
相关资源
最近更新 更多