How can I achieve sprite composition that actually works? - c#

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 (?).

After even more research, I appear to have an answer.
First, some explanation about drawing order: as far as I can tell now, it was consistent in Attempt 4, but invalidating by resizing the window causes gradual invalidations of different areas, which produces misleading results. The order is always top to bottom, which is the opposite of what you want for compositing, but which enables optimizations when you treat everything as opaque by clipping the invalid regions. (I'm not sure how much CPU effort this really saves given that the clipping paths can potentially get quite complex, but I guess it also helps reduce flickering when you aren't using double-buffering, since you avoid repainting a given pixel on any given refresh.)
But as it happens, if you read between the lines, you discover that there is another flag in the "extended window styles" (the 0x20 flag that we set in Attempt 4 is WS_EX_TRANSPARENT), called WS_EX_COMPOSITED (value 0x02000000), that reverses the drawing order to be what we want instead. Or you could have just read the documentation more closely in the first place. Oops.
(You'd think they could have just designed it to take care of all this logic automatically - detect which sprites have a translucent or transparent background colour, draw the ones with opaque backgrounds top-down first for speed, then the others bottom-up for correctness while clipping around the opaque ones. Oh well.)
A quick note here: because compositing naturally involves repainting the same area multiple times, this flag also sets up double-buffering that applies across all children of the window created with that flag (as far as I can tell, in Windows API parlance, every control is a "window", as well as the forms themselves). So it seems that people often use it to avoid flickering on forms with lots of controls, even when they don't intend for controls to overlap. However, ironically, WS_EX_COMPOSITED might actually cause huge amounts of flickering with certain controls, such as TabPage. (I'm running 8.1 and was able to reproduce the described problem very clearly.) The documentation states
This cannot be used if the window has a class style of either CS_OWNDC or CS_CLASSDC.
So maybe that has something to do with it. We can avoid this by restricting the "composited area" to a region using a simple control such as a Panel. At least in my tests, that will fix the problem as long as the TabPage isn't a child of the Panel, even if it overlaps.
It also seems that this might not work under XP, but hey, it's 2014 now.
Anyway, on to the code.
What we need to do is set a CreateWindowEx flag, with the same CreateParams technique, in some ancestor of the Sprite, such as the Form they're on:
protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
cp.ExStyle = cp.ExStyle | 0x02000000;
return cp;
}
}
We still need the WS_EX_TRANSPARENT setting, and disabling of background paint on the Sprite controls, from attempts 3 and 4. We don't need to set up a transparent BackColor, because we're not going to paint the BackColor. I have no idea why allowing a fully transparent BackColor to be painted messes things up, but it apparently does.
This will apparently work even if there are intervening controls, i.e. if we put the Sprite controls inside a Panel on the Form. (If we want to restrict the effect to a Panel, as discussed above, we'd have to subclass Panel; but my project already does so :) ) Everything works perfectly in my test project, and I don't even need to explicitly Invalidate anything.

Related

Line gets drawn only after Control moves over it

I have a set of custom controls and a list of geometric objects that I need to draw on the same handle.
I override the OnPaint procedure, put the base.OnPaint(e) in the first line and commence with the drawing of the geometric objects (via e.Graphics.DrawLine etc.).
Those are in a locked list and decay after a while.
Also the customized controls can move around the window.
Now this is almost working as intended except for this fun fact:
The geometric shapes appear only after a control is moved along/above their layouts.
I was able to reproduce this in a small environment: PASTEBIN
I tried flushing the graphics object; save/restore; changing Clip.
Nothing seemed to work.
I am guessing that regions only get invalidated and repainted once a control is present. But how can I force them to be drawn anyways?
Control.Invalidate will tell the framework that the control needs to be redrawn. It does this automatically to redraw controls after, for example, the mouse has obscured part of it, but it will only redraw a the small section that the mouse covered (hence why you end up with a "(re)painting with the mouse" effect. Also moving a window will force it to redraw, as does covering it with another window and then moving that window away again.
But if you are doing a bunch of custom painting, you need to let it know that the control needs to be redrawn by calling Invalidate yourself.

TabControl flickering

I have a TabControl in which I add / remove several TabPages.
I get flickering issue when I add enough pages so that the navigation buttons have to be shown.
I have no flickering at all when the navigation buttons (2 arrows to navigate left - right) are not shown. The flicker is in no way related to resize of the form or pages being added.
I have tried:
DoubleBuffering
SetStyles(... all the params to increase performance)
EDIT: In my main WinForm, I added this code to prevent a rendering bug when resizing my window:
protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
cp.ExStyle |= 0x02000000;
return cp;
}
}
When removed, I no longer get the flickering in my TabControl. It seems that I will have to live with the resizing rendering bug as it is more convenient that a constant flicker.
You left out an important detail. This question strongly suggests you did more than you described, you are also overriding the CreateParams property in your form to turn on the WS_EX_COMPOSITED style flag for the window. It enables double-buffering for the form and all of its controls, a good way to suppress flicker in general.
But it can have some detrimental side-effects. TabControl is in fact a trouble-maker, something messed up about the way it renders that is incompatible with WS_EX_COMPOSITED. Once too many tabs are added and the navigation buttons appear, it starts to constantly redraw itself. Perceived as rapid flicker. The native tab control renderer has lots of bugs, the Microsoft team that wrote it did a very lousy job.
There is no known workaround for this bug, you'll have to settle for a lesser alternative.
Hi May be I am a bit late to answer, but I used this.Multiline = true; in my TabControl. This will allow TabControl to automatically adjust Tabs in multiline in case number of tabs goes more that available visual space.
You shall use this feature unless you have very specific requirement to keep all tabs in a single line.
The code above helped me resolved Tab flickering issue.I have two tabs in total (in one tab control).Tab names are being redrawn in a loop. The issue is more evident as form(window)'s area is increased.

Is their a way to stop the picturebox from flickering when being resized?

I have an image that I wanted to include in my vb.net app, so I sliced it up in photoshop and divided it into multiple picture boxes, and anchored them accordingly so when my application is resized it wont stretch all parts of the image. That is all good, it looks great and almost works great..except the fact that when the form is being resized it is causing the pictureboxes to flicker.
I know that the picturebox is not the fastest control, so I am geussing it is not refreshing fast enough. Aside from flickering it appears with a white background underneath even though the pictureboxes are transparent.
I tried adding a BG colour for the background hoping it would hide the flickering better while loading to no avail.
So my fist question would be is their any way of preventing this? If not is their a control that is faster that I could use?
Maybe a custom picture box someone knows, or even if you know a control thats faster. Basically any control that would allow a background image and transparent BG color would work as long as it is faster.
I really appreciate any help. Thanks. PS: My application is in VB.net but I am adding a C# tag aswell because I am most likely going to have to switch controls instead of repairing it through code.
Two avoid flickering in controls in form u can use following function
Only copy it and paste it anywhere in form.vb
Protected Overrides ReadOnly Property CreateParams() As CreateParams
Get
Dim cp As CreateParams = MyBase.CreateParams
cp.ExStyle = cp.ExStyle Or &H2000000
Return cp
End Get
End Property 'CreateParams
only paste anwhere it is readonly property
Two ways to handle this are:
a) Resize the image in the picturebox so it is smaller and will redraw faster, or
b) Use a timer to redraw the image so it doesn't begin to redraw until 100 to 350 ms after the last resize event.

Bug or am I doing something wrong? Usercontrol Painting

private void UserControl1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.DrawEllipse(Pens.Black, new Rectangle(-200, -500, this.Width + 400, this.Height + 420));
}
Paste the above code into a usercontrol. Drop the usercontrol onto a form and anchor it to all 4 points.
In the designer (Under Visual Studio 2010) it renders perfectly (even as you resize). Run it and try and resize the form and the ellipse becomes skewed.
Here are two examples both after resizes the first while running, the second in the designer.
Obviously the behavior in the designer can't always be assumed to be the same (Although it would be nice) but my understanding is that the code above is completely legal. Am I wrong?
Stecya has posted the "fix", but omitted an explanation detailing why that works or why you're seeing a discrepancy between the behavior in the designer and in the running application. In my mind, that makes the answer only minimally useful, so I thought I'd weigh in with an attempt at an explanation.
You already know that calling the Invalidate method is the solution. What this does is tell Windows that the entire surface area of the control needs to be redrawn the next time the window gets painted. Simple enough, but why don't you have to do this in the designer?
The answer lies in the fact that you're running the program with the Windows Aero theme enabled. Aero uses an entirely new window manager called the Desktop Window Manager (or DWM for short) based on composition. Each window is double-buffered, meaning that its graphics are rendered off-screen into a temporary bitmap, and only then blitted to the screen. This allows all sorts of cool effects and fancy transitions that users seem to like nowadays.
But, of course, it means that the sections that have already been drawn aren't erased and redrawn unless you explicitly instruct Windows that it needs to do so. That's not a problem for the form inside the designer, because there, Aero's DWM composition is not enabled. When the window gets resized, it is automatically redrawn, and your swoosh looks smooth and correct.
Outside of the designer, with Aero composition enabled, only the newly-exposed portions of your control are redrawn (the rest are still there in the buffer), so the shape is wrong. Part of the old shape is still there, and part of the new shape was just drawn in. Calling Invalidate tells Windows "the graphic surface of this control has changed; forget everything you thought you knew about it and redraw it from scratch next time". So Windows dutifully obeys, discarding that portion from its off-screen buffer and redraws it in from scratch, producing a correctly-rendered, nicely-smoothed path.
You can effect the same change in another somewhat more elegant way: Tell the control that it needs to redraw itself every time that it is resized. Insert the following code into the control's constructor method:
public MyUserControl()
{
// Force the control to redraw itself each time it is resized
this.SetStyle(ControlStyles.ResizeRedraw);
}
You can manually Invalidate control while resizing
private void Form1_Resize(object sender, EventArgs e)
{
userControl11.Invalidate();
}

Creating Overlay Control in winforms

I am using c# winforms to show an Image. The displaying of the image is done using a user control. Now I want to provide the user to draw lines, put other small images, write text etc over the image on an overlay control. How can I provide this functionality? If I use another user control to show the overlay control with transparent back, will that work?? any other solution will be welcome.
You might try approaching this with a canvas (Panel) that handles painting the image as the background and all the annotations/markup afterwards. This will make the foreground appear to be transparent. I expect you'll want to set Control.DoubleBuffer for performance.
You might experiment with setting the style ControlStyles.AllPaintingInWmPaint. Also, try overriding Control.OnPaintBackground and do nothing, and override Control.OnPaint and do all your painting inside there.
If performance is still unacceptable, pay close attention to the PaintEventArgs.ClipRect property. This is the only area you need to paint. The trick is figuring out which of your annotations/overlays intersect with this rectangle and painting them in the correct order.
Either this canvas or a higher level control will need to track mouse movement so you know where to draw the lines, paste images, etc.

Categories

Resources