dotnet / winforms

Windows Forms is a .NET UI framework for building Windows desktop applications.
MIT License
4.3k stars 955 forks source link

`Cursor.Draw` draws with broken alpha channel #10360

Open DJm00n opened 8 months ago

DJm00n commented 8 months ago

.NET version

7.0.400

Did it work in .NET Framework?

No

Did it work in any of the earlier releases of .NET Core or .NET 5+?

No response

Issue description

Curosor.Draw draws with broken alpha channel: Result: image Expected: image

Also reported here more that 10 years ago: https://stackoverflow.com/questions/4451839/how-to-render-a-transparent-cursor-to-bitmap-preserving-alpha-channel

Steps to reproduce

Cursor cursor = Cursors.AppStarting;
Bitmap bitmap = new Bitmap(cursor.Size.Width, cursor.Size.Height, PixelFormat.Format32bppArgb);
Graphics g = Graphics.FromImage(bitmap);
cursor.Draw(g, new Rectangle(0, 0, bitmap.Width, bitmap.Height));
bitmap.Save(Path.Combine(Directory.GetCurrentDirectory(), "cursor.png"), ImageFormat.Png);

This code with Icon.FromHandle and Icon.ToBitmap works as expected:

Cursor cursor = Cursors.AppStarting;
Icon icon = Icon.FromHandle(cursor.Handle);
Bitmap bitmap = icon.ToBitmap();
bitmap.Save(Path.Combine(Directory.GetCurrentDirectory(), "icon.png"), ImageFormat.Png);
elachlan commented 8 months ago

@Olina-Zhang can your team please verify the issue in later versions?

Amy-Li03 commented 8 months ago

This issue reproes on .NET 9.0 latest build: 9.0.100-alpha.1.23564.26. Also, as customer said, this is a not regression issue, it reproes both from .NET 5.0 ~ 9.0 and .NET Framework 4.6.2 ~ 4.8.1.

elachlan commented 8 months ago

Thanks, I was wondering if we had managed to fix it in recent versions. Looks like not.

This is similar to #9421, where we had issues scaling the icon.

elachlan commented 8 months ago

After going over the code, this is by design. GDI+ DrawIcon handles transparency using a single color marked as transparent.

What is weird is this works:

 Cursor cursor = Cursors.AppStarting;
 Icon icon = Icon.FromHandle((IntPtr)cursor.Handle);
 Bitmap bitmap = new Bitmap(cursor.Size.Width, cursor.Size.Height, PixelFormat.Format32bppArgb);
 Graphics g = Graphics.FromImage(bitmap);
 g.DrawIcon(icon, new Rectangle(0, 0, bitmap.Width, bitmap.Height));
 bitmap.Save(Path.Combine(Directory.GetCurrentDirectory(), "cursor2.png"), ImageFormat.Png);

I'll need to debug/investigate more but that is what I have found so far.

DJm00n commented 8 months ago

@elachlan Graphics.DrawIcon is clearly contains some kind of workaround (with use of Icon.ToBitmap() under the hood) for this issue:

https://github.com/dotnet/winforms/blob/d20f9c71a189693c6c3475253397665bce0a1354/src/System.Drawing.Common/src/System/Drawing/Graphics.cs#L3213-L3228

elachlan commented 8 months ago

It looks like Graphics.FromImage(bitmap); sets the _backingImage which is then used in the code you linked.

I can't see an obvious solution in Cursor because we don't know how Graphics was created. Graphics g = Graphics.FromImage(bitmap); basically sets that context.

I guess we could just call the graphics.drawicon from cursor? or expose a bool indicating if _backingImage is being used, which we can use to call DrawImage?

elachlan commented 8 months ago

Kind of related: #9879 and #10293

elachlan commented 8 months ago

@DJm00n would a new API Cursor.ToBitmap be a good solution?

Cursors.AppStarting.ToBitmap().Save(Path.Combine(Directory.GetCurrentDirectory(), "cursor2.png"), ImageFormat.Png);
DJm00n commented 8 months ago

@elachlan I think adding Cursor.ToBitmap (and fixing Cursor.Draw?) would be a good solution. Actually most of the cursor and icon code could be merged into one base class since they are working on the same Win32 APIs under the hood. Looks like they are separated by historical reasons.

JeremyKuhne commented 7 months ago

Adding a Cursor.ToBitmap seems to fit with the existing Icon.ToBitmap.

Looking at it a bit it appears DrawIconEx might do the right thing? DrawIcon basically does this: DrawIconEx(hdc, x, y, hicon, 0, 0, 0, 0, DI_NORMAL | DI_COMPAT | DI_DEFAULTSIZE );. DI_COMPAT prevents alpha channel support I believe.

DJm00n commented 7 months ago

@JeremyKuhne AFAIK DI_COMPAT does nothing. It was a thing in pre-Windows NT era.

DJm00n commented 7 months ago

@JeremyKuhne Icon.ToBitmap is crazy stuff:

https://github.com/dotnet/winforms/blob/main/src/System.Drawing.Common/src/System/Drawing/Icon.cs#L687-L844

I think it implementation could be merged with future Cursor.ToBitmap.

JeremyKuhne commented 7 months ago

AFAIK DI_COMPAT does nothing. It was a thing in pre-Windows NT era.

Yeah, I read the code wrong, I don't see it doing anything.

I think it implementation could be merged with future Cursor.ToBitmap.

I wonder if all of that is actually necessary or if there are alternative APIs we can be using. In general, of course, we should not duplicate code.

DJm00n commented 7 months ago

Related:

https://stackoverflow.com/a/11338010/1795050

https://web.archive.org/web/20120201182215/http://connect.microsoft.com/VisualStudio/feedback/details/97232/graphics-drawicon-will-not-render-alpha-icon-correctly-when-graphics-is-a-bitmap