【发布时间】: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 的答案,如 this 和 this,文章如 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 建议的那样,您应该在托管它们的单个控件中自己管理精灵。