Farbverlauf von mehreren Farben ins Transparente

  • C#
  • .NET (FX) 1.0–2.0

Es gibt 41 Antworten in diesem Thema. Der letzte Beitrag () ist von ErfinderDesRades.

    Oben hast Du es richtig gemacht, unten nicht.
    Mehrmals if, wo eine else if hingehört:
    Spoiler anzeigen

    C#-Quellcode

    1. public class LockBitmap
    2. {
    3. Bitmap source = null;
    4. IntPtr Iptr = IntPtr.Zero;
    5. BitmapData bitmapData = null;
    6. public byte[] Pixels { get; set; }
    7. public int Depth { get; private set; }
    8. public int Width { get; private set; }
    9. public int Height { get; private set; }
    10. public LockBitmap(Bitmap source)
    11. {
    12. this.source = source;
    13. }
    14. /// <summary>
    15. /// Lock bitmap data
    16. /// </summary>
    17. public void LockBits()
    18. {
    19. try
    20. {
    21. // get source bitmap pixel format size
    22. Depth = System.Drawing.Bitmap.GetPixelFormatSize(source.PixelFormat);
    23. // Check if bpp (Bits Per Pixel) is 8, 24, or 32
    24. if (Depth != 8 && Depth != 24 && Depth != 32)
    25. {
    26. throw new ArgumentException("Only 8, 24 and 32 bpp images are supported.");
    27. }
    28. // Get width and height of bitmap
    29. Width = source.Width;
    30. Height = source.Height;
    31. // get total locked pixels count
    32. int PixelCount = Width * Height;
    33. // Create rectangle to lock
    34. Rectangle rect = new Rectangle(0, 0, Width, Height);
    35. // Lock bitmap and return bitmap data
    36. bitmapData = source.LockBits(rect, ImageLockMode.ReadWrite,
    37. source.PixelFormat);
    38. // create byte array to copy pixel values
    39. int step = Depth / 8;
    40. Pixels = new byte[PixelCount * step];
    41. Iptr = bitmapData.Scan0;
    42. // Copy data from pointer to array
    43. Marshal.Copy(Iptr, Pixels, 0, Pixels.Length);
    44. }
    45. catch (Exception ex)
    46. {
    47. throw ex;
    48. }
    49. }
    50. /// <summary>
    51. /// Unlock bitmap data
    52. /// </summary>
    53. public void UnlockBits()
    54. {
    55. try
    56. {
    57. // Copy data from byte array to pointer
    58. Marshal.Copy(Pixels, 0, Iptr, Pixels.Length);
    59. // Unlock bitmap data
    60. source.UnlockBits(bitmapData);
    61. }
    62. catch (Exception ex)
    63. {
    64. throw ex;
    65. }
    66. }
    67. /// <summary>
    68. /// Get the color of the specified pixel
    69. /// </summary>
    70. /// <param name="x"></param>
    71. /// <param name="y"></param>
    72. /// <returns></returns>
    73. public Color GetPixel(int x, int y)
    74. {
    75. Color clr = Color.Empty;
    76. // Get color components count
    77. int cCount = Depth / 8;
    78. // Get start index of the specified pixel
    79. int i = ((y * Width) + x) * cCount;
    80. if (i > Pixels.Length - cCount)
    81. {
    82. throw new IndexOutOfRangeException();
    83. }
    84. if (Depth == 32) // For 32 bpp get Red, Green, Blue and Alpha
    85. {
    86. byte b = Pixels[i];
    87. byte g = Pixels[i + 1];
    88. byte r = Pixels[i + 2];
    89. byte a = Pixels[i + 3]; // a
    90. clr = Color.FromArgb(a, r, g, b);
    91. }
    92. else if (Depth == 24) // For 24 bpp get Red, Green and Blue
    93. {
    94. byte b = Pixels[i];
    95. byte g = Pixels[i + 1];
    96. byte r = Pixels[i + 2];
    97. clr = Color.FromArgb(r, g, b);
    98. }
    99. else if (Depth == 8) // For 8 bpp get color value (Red, Green and Blue values are the same)
    100. {
    101. byte c = Pixels[i];
    102. clr = Color.FromArgb(c, c, c);
    103. }
    104. return clr;
    105. }
    106. /// <summary>
    107. /// Set the color of the specified pixel
    108. /// </summary>
    109. /// <param name="x"></param>
    110. /// <param name="y"></param>
    111. /// <param name="color"></param>
    112. public void SetPixel(int x, int y, Color color)
    113. {
    114. // Get color components count
    115. int cCount = Depth / 8;
    116. // Get start index of the specified pixel
    117. int i = ((y * Width) + x) * cCount;
    118. if (Depth == 32) // For 32 bpp set Red, Green, Blue and Alpha
    119. {
    120. Pixels[i] = color.B;
    121. Pixels[i + 1] = color.G;
    122. Pixels[i + 2] = color.R;
    123. Pixels[i + 3] = color.A;
    124. }
    125. else if (Depth == 24) // For 24 bpp set Red, Green and Blue
    126. {
    127. Pixels[i] = color.B;
    128. Pixels[i + 1] = color.G;
    129. Pixels[i + 2] = color.R;
    130. }
    131. else if (Depth == 8) // For 8 bpp set color value (Red, Green and Blue values are the same)
    132. {
    133. Pixels[i] = color.B;
    134. }
    135. }
    136. }
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!

    TRiViUM schrieb:

    aber so ist's deshalb auch sauberer.
    Auch schneller.
    GetPixel() und SetPixel() kommen ja gehäuft vor, und dann stets 3 if-s auszuwerten ist länglich.
    Wenn ich mit 6 oder 16 BPP Mono-Bildern arbeite, arbeite ich mit byte bzw. ushort, da kommt Color nur als Palette-Eintrag vor.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!

    TRiViUM schrieb:

    Jetzt ist es aber lauffähig


    Unter "lauffähig" verstehe ich eiglich einen gezippten Ordner mit einer .sln-Datei darin, die ich im VS öffnen kann - F5 - und läuft.
    Nicht eine verlorene .cs-Datei, wo ich erstnoch ein Projekt drumherum stricken muss, bevor überhaupt ein Compiler mir sagt, dass der Code überhaupt syntaktisch korrekt ist.

    Aber seisdrum - ist in diesem Falle ja nicht soo der Act, was drumherum zu stricken
    So, jetzt habih bischen mehr geguckt und MemoryLeak gefunden:
    In jedem PaintEvent wird eine neue Bitmap erzeugt, gelockt, bepixelt, ins Graphics gezeichnet - und nicht mehr disposed.
    Das mit dem Disposen ist noch recht einfach zu fixen, aber das ganze Brimborium was da getrieben wird - das sollte nur ein einziges Mal getrieben werden.
    Dann wäre die Bitmap fertig, und könnte in weiteren OnPaint()-Aufrufen immer wieder ins Graphics gezeichnet werden.
    Allerdings müsste man Aufwand treiben, um die Bitmap immer genau dann zu erneuern, wenn sich Rahmenbedingungen ändern (ColorChanged, SizeChanged, ...)

    Auch nicht schön, dass die Klasse LockBitmap in derselben Datei wie class AmbiFrameControl quasi "versteckt" ist.

    Und sowas (an mehreren Stellen gefunden):

    C#-Quellcode

    1. try {
    2. // Copy data from byte array to pointer
    3. Marshal.Copy(Pixels, 0, Iptr, Pixels.Length);
    4. // Unlock bitmap data
    5. source.UnlockBits(bitmapData);
    6. }
    7. catch (Exception ex) {
    8. throw ex;
    9. }
    ist ja Quatsch.
    Wenn man ex nur weiter-wirft, dann braucht man sie ja garnet erst zu fangen - das würde den Code auch wieder deutlich vereinfachen.

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „ErfinderDesRades“ ()

    RodFromGermany schrieb:

    3 if-s auszuwerten ist länglich
    Da die GetPixel-Funktion relativ oft angewendet wird, auf jeden Fall, danke!

    ErfinderDesRades schrieb:

    wo ich erstnoch ein Projekt drumherum stricken muss
    Jo, da gebe ich dir Recht. Hab ich mir aber für's nächste Mal notiert :)

    ErfinderDesRades schrieb:

    mehr geguckt und MemoryLeak gefunden
    Oh, danach hatte ich noch gar nicht Ausschau gehalten, aber hab jetzt ein using-Block für die Verwendung der Bitmap angelegt.

    ErfinderDesRades schrieb:

    das sollte nur ein einziges Mal getrieben werden.
    Das sollte man definitiv noch optimieren.
    Hatte ich anfangs auch so vor gehabt, bis ich festgestellt hatte, dass das OnPaint-Event vor dem Klassen-Konstruktor kommt (zumindest, wenn man das Control über den Designer aus der Toolbox aufs Form platziert).
    Zu diesem Zeitpunkt ist die Farbliste noch nicht initialisiert und das Control ist kaputt.
    Wie hast Du dir das vom Aufbau vorgestellt? Hab vermutlich nur einen Knoten im Hirn :S

    ErfinderDesRades schrieb:

    wenn sich Rahmenbedingungen ändern (ColorChanged, SizeChanged, ...)
    Das sollte ja auch gar nicht allzu viel sein, zudem ich ColorChanged schon mitbekomme.

    Beim SizeChanged hab ich ne Frage.
    Ich hab im Klassen-Konstruktor folgenden Code:

    C#-Quellcode

    1. SetStyle( ControlStyles.SupportsTransparentBackColor |
    2. ControlStyles.OptimizedDoubleBuffer |
    3. ControlStyles.UserPaint |
    4. ControlStyles.AllPaintingInWmPaint |
    5. ControlStyles.ResizeRedraw


    Bewirkt das Flag ControlStyles.ResizeRedraw hier automatisch, dass bei Größenveränderung das OnPaint-Event aufgerufen wird?
    Und sollte man anstelle dessen doch lieber folgendes nutzen:

    C#-Quellcode

    1. protected override void OnSizeChanged(EventArgs e)
    2. {
    3. base.OnSizeChanged( e );
    4. }
    Nur so würde ich ja mitbekommen, dass sich die Größe verändert hat, oder?

    ErfinderDesRades schrieb:

    Klasse LockBitmap in derselben Datei wie class AmbiFrameControl quasi "versteckt"
    Das hatte ich tatsächlich nur schnell gemacht, damits "lauffähig" wird.
    Im Ursprungsprojekt war noch mehr drumherum, weshalb ich das nicht angehangen hatte.
    Aber ich habs jetzt mal abgespalten und angehangen.

    Achja, und die fragwürdigen try-catch-Anweisungen hab ich auch rausgenommen.
    Dateien

    TRiViUM schrieb:

    bis ich festgestellt hatte, dass das OnPaint-Event vor dem Klassen-Konstruktor kommt
    Das klingt sehr merkwürdig.
    Allerdings gibt es eine Component.DesignMode, die kannst Du abfragen.
    ====
    Du berechnest für jedes Pixel einzeln den Transparenzwert, es genügt doch, dies für jede Zeile bzw. Spalte zu tun.
    Ordne die Schleifen entsprechend und pack die Transparenz-Berechnung in die äußere Schleife.
    ====

    TRiViUM schrieb:

    Bewirkt das Flag ControlStyles.ResizeRedraw hier automatisch, dass bei Größenveränderung das OnPaint-Event aufgerufen wird?
    Das kannst Du selbst ausprobieren, indem Du das Flag einfach weg lässt. ;)
    Die Prozedur OnSizeChanged() kannst Du dann ganz weglassen.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!

    RodFromGermany schrieb:

    Das klingt sehr merkwürdig.
    Allerdings.
    Vielleicht deshalb, weil beim Platzieren eines Controls erst mal was im Designer angezeigt wird? Ich hab keine Ahnung, hab es aber mit MessageBoxen im Konstruktor und im PaintEvent festgestellt, nachdem ich ne Exception bekommen hatte, dass die Farbliste nicht auf ne Instanz verweist (wird im Konstruktor zugewiesen)...

    RodFromGermany schrieb:

    Du berechnest für jedes Pixel einzeln den Transparenzwert, es genügt doch, dies für jede Zeile bzw. Spalte zu tun.
    Oh man, du hast Recht, ändere ich sofort ab. Sollte einen großen Performanceschub bringen, zudem es unnötig ist, für jeden Pixel zu berechnen...

    RodFromGermany schrieb:

    Die Prozedur OnSizeChanged() kannst Du dann ganz weglassen.
    Nicht, wenn ich den Gedankengang von @ErfinderDesRades weiter verfolge.
    Sonst bekomme ich ja nicht mehr mit, dass sich die Größe des Steuerelements geändert hat.
    Dann sollte ich vermutich eher das ​ControlStyles.ResizeRedraw weglassen.

    TRiViUM schrieb:

    dass die Farbliste nicht auf ne Instanz verweist
    Das ist doch ne klare Aussage.
    Also solltest Du nach dem Aufruf von InitializeComponents() die Farbliste bereitstellen oder Du nimmst ein (Not)Ready-Flag.
    Die erstere Variante ist da allerdings sinnvoller.
    Musst Du wissen, dass sich die Größe des Controls verändert hat, nur um das Control neu zu zeichnen oder hat das eine weitere Bewandtnis?
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!

    TRiViUM schrieb:

    das sollte nur ein einziges Mal getrieben werden.

    Ich würde eine CacheBitmap einführen, sowie ein IsDirty-Flag.
    Wenn im OnPaint das Flag auf True steht, dann die CacheBitmap vor dem eigentlichen Zeichenvorgang noch erneuern (und dann das Flag auf false setzen).
    In verschiedenen Events/Overrides (OC_CollectionChanged, Control_SizeChanged) dann das Flag true setzen.

    RodFromGermany schrieb:

    die Farbliste bereitstellen
    Ich glaub, wir reden hier vermutlich nicht vom Gleichen. Das Control selbst stellt ja die Farbliste bereit, die im Konstruktor instanziiert und auch als Property bereitgestellt wird.

    RodFromGermany schrieb:

    hat das eine weitere Bewandtnis?
    Ja, siehe Post 30.

    @ErfinderDesRades
    Ah okay und das OnPaint-Event dann mit Invalidate() auslösen?

    Dieser Beitrag wurde bereits 4 mal editiert, zuletzt von „TRiViUM“ ()

    @ErfinderDesRades Jou, hab mal das aktuelle Projekt angehangen, sollte so passen.

    Eine letzte Frage in der Render-Funktion:
    Dort habe ich momentan noch folgendes stehen:

    C#-Quellcode

    1. if (cacheBitmap != null)
    2. cacheBitmap.Dispose();
    3. cacheBitmap = new Bitmap( rect.Width, rect.Height, PixelFormat.Format32bppArgb );

    Theoretisch muss diese da nicht mehr disposed werden, oder?

    Wenn man die Anwendung ausführt, hat man jetzt nen PropertyGrid, wo man mit den 3 Properties in der Kategorie "Ambilight" bisserl herumspielen kann...
    Dateien
    @ErfinderDesRades noch mal zum Verständnis:
    Zuerst hatte ich im OnPaint-Event die Bitmap erzeugt und gezeichnet, anfangs auch noch ohne diese nach dem Zeichnen zu disposen.

    Du hattest mich dann auf den MemoryLeak hingewiesen, worauf hin ich die Bitmap im OnPaint-Event in einer Using-Anweisung deklariert habe.
    Und dann kam der Gedanke, die Bitmap nur dann zu generieren, wenn man sie neu generieren muss ((OC_CollectionChanged, Control_SizeChanged)).

    Dazu hab ich mir ne Methode geschrieben, die das Bild zeichnet und in die Klassenweite Variable ChacheBitmap ablegt, welche dann später vom OnPaint-Event zum Zeichnen benutzt wird.
    Wann wäre nun der richtige Zeitpunkt, die CacheBitmap zu disposen ?

    Exemplarisches Beispiel am aktuellen Projekt:
    Durch Änderung der Größe soll die Bitmap neu gezeichnet werden.

    C#-Quellcode

    1. protected override void OnSizeChanged(EventArgs e)
    2. {
    3. base.OnSizeChanged( e );
    4. ReDraw();
    5. }


    Die Methode ReDraw():

    C#-Quellcode

    1. private void ReDraw()
    2. {
    3. isBitmapCacheDirty = true;
    4. Invalidate();
    5. }


    Das OnPaint-Event:

    C#-Quellcode

    1. protected override void OnPaint(PaintEventArgs e)
    2. {
    3. base.OnPaint( e );
    4. if (isBitmapCacheDirty) RenderBitmap();
    5. if ( cacheBitmap != null)
    6. e.Graphics.DrawImageUnscaled( cacheBitmap, 0, 0 );
    7. }


    Und die Methode RenderBitmap():
    Anfang:

    C#-Quellcode

    1. private void RenderBitmap()
    2. {
    3. Rectangle rect = new Rectangle( 0, 0, Width, Height );
    4. if (cacheBitmap != null)
    5. cacheBitmap.Dispose();
    6. cacheBitmap = new Bitmap( rect.Width, rect.Height, PixelFormat.Format32bppArgb );


    Komplett
    Spoiler anzeigen

    C#-Quellcode

    1. private void RenderBitmap()
    2. {
    3. Rectangle rect = new Rectangle( 0, 0, Width, Height );
    4. if (cacheBitmap != null)
    5. cacheBitmap.Dispose();
    6. cacheBitmap = new Bitmap( rect.Width, rect.Height, PixelFormat.Format32bppArgb );
    7. if (gradientColors.Count >= 2 && gradientColors[gradientColors.Count - 1] != Color.Empty)
    8. {
    9. #region gradient
    10. using (Graphics g = Graphics.FromImage( cacheBitmap ))
    11. {
    12. g.Clear( Color.Transparent );
    13. int angle = gradientOrientation == Orientation.Vertical ? 90 : 0;
    14. LinearGradientBrush br = new LinearGradientBrush( rect, Color.Transparent, Color.Transparent, angle, false );
    15. br.WrapMode = WrapMode.Tile;
    16. ColorBlend cb = new ColorBlend();
    17. float[] positions = new float[gradientColors.Count];
    18. Color[] colors = new Color[gradientColors.Count];
    19. for (int i = 0; i < gradientColors.Count; i++)
    20. {
    21. colors[i] = gradientColors[i];
    22. positions[i] = i / (float)( positions.Length - 1 );
    23. }
    24. cb.Positions = positions;
    25. cb.Colors = colors;
    26. br.InterpolationColors = cb;
    27. g.FillRectangle( br, rect );
    28. }
    29. #endregion
    30. #region transparency
    31. LockBitmap lockedBmp = new LockBitmap( cacheBitmap );
    32. lockedBmp.LockBits();
    33. if (transparencySide == TransparencySides.Top)
    34. {
    35. int start = lockedBmp.Height / 2;
    36. for (int y = start; y >= 0; y--)
    37. {
    38. byte currentTransparency = (byte)Map( y, start, 0, 255, 0 );
    39. for (int x = 0; x < lockedBmp.Width; x++)
    40. {
    41. Color pixel = lockedBmp.GetPixel( x, y );
    42. pixel = Color.FromArgb( currentTransparency, pixel.R, pixel.G, pixel.B );
    43. lockedBmp.SetPixel( x, y, pixel );
    44. }
    45. }
    46. }
    47. else if (transparencySide == TransparencySides.Bottom)
    48. {
    49. int start = lockedBmp.Height / 2;
    50. for (int y = start; y < lockedBmp.Height; y++)
    51. {
    52. byte currentTransparency = (byte)Map( y, start, lockedBmp.Height, 255, 0 );
    53. for (int x = 0; x < lockedBmp.Width; x++)
    54. {
    55. Color pixel = lockedBmp.GetPixel( x, y );
    56. pixel = Color.FromArgb( currentTransparency, pixel.R, pixel.G, pixel.B );
    57. lockedBmp.SetPixel( x, y, pixel );
    58. }
    59. }
    60. }
    61. else if (transparencySide == TransparencySides.Left)
    62. {
    63. int start = lockedBmp.Width / 2;
    64. for (int x = start; x >= 0; x--)
    65. {
    66. byte currentTransparency = (byte)Map( x, 0, start, 0, 255 );
    67. for (int y = 0; y < lockedBmp.Height; y++)
    68. {
    69. Color pixel = lockedBmp.GetPixel( x, y );
    70. pixel = Color.FromArgb( currentTransparency, pixel.R, pixel.G, pixel.B );
    71. lockedBmp.SetPixel( x, y, pixel );
    72. }
    73. }
    74. }
    75. else if (transparencySide == TransparencySides.Right)
    76. {
    77. int start = lockedBmp.Width / 2;
    78. for (int x = start; x < lockedBmp.Width; x++)
    79. {
    80. byte currentTransparency = (byte)Map( x, start, lockedBmp.Width, 255, 0 );
    81. for (int y = 0; y < lockedBmp.Height; y++)
    82. {
    83. Color pixel = lockedBmp.GetPixel( x, y );
    84. pixel = Color.FromArgb( currentTransparency, pixel.R, pixel.G, pixel.B );
    85. lockedBmp.SetPixel( x, y, pixel );
    86. }
    87. }
    88. }
    89. lockedBmp.UnlockBits();
    90. #endregion
    91. }
    92. isBitmapCacheDirty = false;
    93. }


    Wäre der richtige Moment zum disposen der Bitmap nicht der, nachdem ich die Bitmap gezeichnet habe?

    TRiViUM schrieb:

    Wann wäre nun der richtige Zeitpunkt, die CacheBitmap zu disposen ?
    Immer dann, wenn Du eine neue zeichnen musst und wenn das Control selbst dispost wird. Diese Dispose-Prozedur findest Du in der Datei DEIN_CONTROL.Designer.cs.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!

    TRiViUM schrieb:

    Dann sollte ich wohl IDisposable implementierern?
    Ja, da bist Du auf der sicheren Seite.
    Aber:
    Warum Control und nicht UserControl?
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!

    TRiViUM schrieb:

    Dann sollte ich wohl IDisposable implementierern?
    Nein - IDisposable ist bereits implementiert.
    Im ObjectBrowser kannste nachgucken, durch welche Basisklasse.
    Folglich liegts an dir Dispose(bool) zu überschreiben.

    Zum Code: OnPaint, zeile#7 ist üflüssig.
    Ich find das immer bisserl traurig, dass die Leuts an allen Ecken und Enden iwelche Bedingungen abprüfen, die - wenn man sich den eigenen Code mal angucken täte - überhaupt niemals eintreten können.

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „ErfinderDesRades“ ()

    @TRiViUM Ja so isses, da hat mich einer mit Blindheit geschlagen:
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!