My requirements:
- A control with a background can have sprites drawn on it.
- It needs to be possible to move sprites around programmatically and by setting up dragging events.
- The sprite's image may have alpha transparency; sprites must correctly alpha-blend with both the background and each other.
- Drawing order must match the logical order of sprites - clicking on a sprite that appears to be on top should initiate dragging that sprite, never one that appears underneath it.
Attempt 1
The obvious approach is to create a custom control to represent the Sprite. Let's try it:
public partial class Sprite : Control
{
public Sprite()
{
InitializeComponent();
For testing purposes, we'll make it so clicking a Sprite just brings it to the front, no dragging yet:
this.Click += Sprite_Click;
}
void Sprite_Click(object sender, EventArgs e)
{
this.BringToFront();
}
So we can identify each sprite and verify the drawing order, let's set a 'frame' color for each:
public Color FrameColor { get; set; }
And draw the sprites as a translucent white body with a solid-colour frame - we should then be able to verify that the background appears shaded behind a single space, shaded more strongly where sprites overlap, and the borders overlap as expected:
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);
}
And then we can design a form with a dark background, set up a few Sprites on it, make them overlap, give them different frame colours, and test.
Naturally, it doesn't work. Controls by default have a background colour, which appears to default to white, or something close to it. So these sprites overlap properly, but they have opaque white middles.
Attempt 2
Well, surely we can just set that background colour to transparent? A bit of Googling in Microsoft's documentation tells us that we certainly can, but not directly (as someone else on SO found out the hard way). It needs a bit of configuration:
// in constructor
SetStyle(ControlStyles.SupportsTransparentBackColor, true);
this.BackColor = Color.Transparent;
Okay, so now the background shows through each Sprite, but the Sprite borders don't show through each other. What gives? A little more Googling tells us that this 'transparent background colour' support is actually a bit of a hack; basically, the parent Control implements its background painting to copy image data from the parent component and composite with that, ignoring everything else.
To be totally honest, I expected the system to be designed such that this would just work automatically - i.e., any time any drawing occurs, it composites with whatever's underneath it on screen, and the only reason controls aren't constantly showing through each other is because of their explicitly opaque backgrounds. But no such luck. I guess the whole system was designed back when people didn't want that sort of thing by default, for performance reasons.
Anyway.
Attempt 3
Well, if the background painting is what's causing the problem, maybe we can just disable it completely?
protected override void OnPaintBackground(PaintEventArgs ignored) { }
Nope. If we click on sprites, we can see them re-order, and composite with each other - but they don't composite with the background image. And, more strangely, when the parent window is invalidated (by resizing the form, or minimizing and restoring it), the sprites turn opaque again.
Attempt 4
After even more Googling, we find StackOverflow answers like this and this, articles like this etc. And they all point at the same low-level hack:
protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
cp.ExStyle = cp.ExStyle | 0x20;
return cp;
}
}
For this to work, we also need to disable painting the background, but of course it no longer matters if we set up that "transparent" background, since that painting logic has been suppressed. (Curiously, it's also possible to suppress painting the background by setting an Opaque
option in the ControlStyles
; while that sounds like the opposite of what we want, it seems to work about as well.)
Well. It almost works. The Sprites composite both with themselves and the background. But now a very curious thing happens: the drawing order is wrong, and not even consistent. Invalidating the window in different ways (resizing vs. minimizing and restoring) will bring a different Sprite to the front visually; but in general it doesn't correspond to what was clicked and in what order. If we add explicit invalidation logic:
// in click event handler
Parent.Invalidate(this.Bounds, true);
then clicking a sprite actually appears to send it to the back visually - although again, the drawing order may change if we resize the window or minimize and restore it.
What gives? How can I solve this once and for all? Options I've considered:
Burn the controls to the ground; make a parent control keep track of a list of sprite
Images
, track mouse drag and click events, and do all the picking and rendering logic itself. Should be guaranteed to work, but is a ridiculous amount of work for the job.Make the controls not draw at all, by overriding both background and foreground painting, and have them expose a property with their desired Image. Let a parent handle all the rendering, but fall back on the built-in Control logic for picking, mouse drags etc. Less work, but seems iffy (maybe there's some hidden thing that still forces Controls to draw something?), is a fairly tightly coupled design, and might still have performance issues if a naive approach is taken to invalidation (?).