Picturebox als Zeichenfläche => Probleme, Probleme, Probleme ...

  • VB.NET

Es gibt 21 Antworten in diesem Thema. Der letzte Beitrag () ist von picoflop.

    Picturebox als Zeichenfläche => Probleme, Probleme, Probleme ...

    Hallo,

    ich hatte schon einmal über mein Problem geschrieben, das ich mit der Picturebox beim Umstieg von vb6 auf vb.net habe und dank hilfreicher Kommentare habe ich jetzt die Zeichnerei in der Picturebox soweit im Griff, indem ich nur noch mit Pixeln statt wie in vb6 mit komfortableren Millimeter arbeite.
    Probleme macht mir die Geschwindigkeit der Zeichnerei in der Picturebox.

    In vb6 hatte ich Anfangs die gleiche Schwierigkeit, dort hatte ich das folgendermaßen gelöst:
    - 2 Pictureboxen, einmal die für den Benutzer sichtbare (PictureBox1) und eine unsichtbare mit gleichen Einstellungen (PictureBoxBuffer) mit visible=false
    - Zeichnen der Projektelemente (Linien, Rechtecke, Texte,... ca. 20-30 tausend) in unsichtbaren PictureboxBuffer
    - Nachdem alle Elemente gezeichnet sind => PictureBox1.Picture=PictureBoxBuffer.Image (im Prinzip das in der unsichtbaren PB gezeichnete Bild in die sichtbare PB kopieren)
    Das war sauschnell, das Neuzeichnen von ca. 50.000 Elementen erfolgte praktisch ohne Zeitverzug

    Wenn ich jetzt in vb.net den gleichen Ansatz verfolge, habe ich folgende Probleme:
    - .Picture gibt es nicht mehr bei der PictureBox
    - PictureBox1.Image=PictureBoxBuffer.Image führt zu einem weißen Bild
    - Wenn ich die PictureBox1 auf visible=false stelle, dann zeichne und danach visible=true mache, ist die PB ebenfalls leer, irgendwie wird beim Umstellen das eben gezeichnete gelöscht

    Im Moment komme ich nicht richtig weiter und hoffe mir kann jemand einen hilfreichen Tip geben, im Prinzip geht es nur darum sehr viele Zeichenelemente wie Linien oder Rechtecke auf einer PB als Zeichenfläche zu malen und das so schnell, das es (fast) in Echtzeit geschieht.

    Vielen Dank für eure Kommentare im Voraus
    Sascha
    in .Net - WinForms funzt OwnerDrawing ganz anners.

    Probier erstmal mit 1 Objekt: Control mit beweglicher Figur

    Dann kannste zu viele Objekte übergehen: Gezieltes OwnerDrawing

    Aber 50000 findich dochn bischen ville, binnichja gespannt, ob das noch performant wird.

    Annere Frage: wie guckt man sich eine Picturebox an, wo 50000 Objekte gemalt sind? Da braucht man einen haushohen Bildschirm, oder?
    Warum eine unsichtbare Picturebox? Genau die schluckt Resourcen, weil auch das Control selber und nich nur das Bild refresht werden.
    Wenn, dann zeichne auf einem Bitmap und zeichne im Paint das Bitmap auf die Form.
    Noch besser ist graphicspath für die 1/2Müllion Objekte und denn den GraphicsPath in einem Rutsch auf die Form zeichnen.
    Die Form verfügt über Formstyle-Eigenschaft, wo du Backbuffer=true setzen kanst.

    Fiel Fergnügen

    Vatter
    :thumbsup: Seit 26.Mai 2012 Oppa! :thumbsup:
    Danke erst mal für die schnellen Antworten.

    Ich fürchte ich werde mich dann doch erst einmal mit den Beispielen von ErfinderDesRades auseinandersetzen und mir die Funktion graficspath anschauen müssen.
    Nur zu meiner Info und als Vergleich, wie zeichne ich denn auf einer Bitmap, das könnte auch eine interessante Alternative sein?

    Die 50.000 Elemente sind nicht wirklich viel, wenn ich in meinen Projekten zB eine Linie brauche, dann besteht die aus:
    - schwarze Linie Hintergrund 8 Pixel + Linie orange Vordergrund 6 Pixel = Eine Linie mit schwarzem Rand
    - dazu vorne und hinten ein Anschlußkästchen auch mit Rahmen
    Macht für eine Verbindungslinie direkt mal 6 Zeichnungselemente, das läppert sich.


    In vb6 war in einer unsichtbaren Picturebox wesentlich schneller gezeichnet als wenn diese sichtbar ist, bei vb.net scheint dem nicht so zu sein.
    auf eine bitmap zu zeichnen ist sehhhr leicht:

    VB.NET-Quellcode

    1. dim tmpBit as new bitmap(700,600)
    2. dim tmpGra as graphics = graphics.fromimage(tmpBit)
    3. with tmpGra
    4. 'zeichnen
    5. end with


    schau doch mal in mein GDI+ Tutorial rein, da gibts weitere nützliche sachen^^ (=> siehe signatur)

    PeterPan0815 schrieb:

    schwarze Linie Hintergrund 8 Pixel + Linie orange Vordergrund 6 Pixel = Eine Linie mit schwarzem Rand
    Das passt sogar in einen GraphicsPath, den man 1x mit dem einen und 1x mit dem annern Stift malt.
    8-)
    :thumbsup: Seit 26.Mai 2012 Oppa! :thumbsup:
    Erst einmal vielen Dank für den Tip mit dem Zeichnen im Bitmap, das funktioniert perfekt und ist schnell

    'Originalbitmap
    - Dim drawingBMP As New Bitmap(2000, 1500)
    - Dim pdDrawAreaBMP As Graphics = Graphics.FromImage(drawingBMP)

    'Hier wird im Originalbitmap gezeichnet
    - pdDrawAreaBMP.DrawLine(...)
    - pdDrawAreaBMP.FillRectangle(...)
    - PictureBox.Image = drawingBMP 'Bitmap wird auf die Picturebox gelegt

    Und jetzt mein Problem:
    Ich möchte das Original-Bitmap in ein zweites kopieren, dieses sieht folgendermaßen aus:
    'Backup Bitmap
    - Dim drawingBMPbuffer As New Bitmap(2000, 1500)
    - Dim pdDrawAreaBMPbuffer As Graphics = Graphics.FromImage(drawingBMPbuffer)

    Grund ist folgender:
    Solange ich das Original nicht verändern will, zeichne ich im Buffer-Bitmap und weise dies der PictureBox zu. Erst wenn das Ergebnis passt wird die Originalbitmap neu gezeichnet und dann gehts von vorne los.

    Ich bekomme das Bitmap aber nicht kopiert.
    Der simple Versuch drawingBMP = drawingBMPbuffer führt zu nichts. Auch nach einigem googeln haben andere Wege, wie zB .clone oder DirectCast nicht zum Erfolg geführt.
    Das einzige was funktionierte (ist aber nicht praktikabel, da sterbenslangsam), war das Original-Bitmap Pixel für Pixel ins Bitmap-Buffer zu kopieren.

    Ich denke mir fehlt einfach nur die richtige Vorgehensweise, vielleicht kann mir jemand helfen.
    Also wenn du eine Art Designer basteln willst, für derlei komplizierte Graphen wie in Post#4 gezeigt, dann ist Bitmap ein Holzweg.
    Weil aus Bitmap kannst du nix rauslöschen, und natürlich will man, wenn man einen Knoten falsch gesetzt hat, den auch jederzeit wieder wegmachen können.

    ErfinderDesRades schrieb:

    Also wenn du eine Art Designer basteln willst, für derlei komplizierte Graphen wie in Post#4 gezeigt, dann ist Bitmap ein Holzweg.
    Weil aus Bitmap kannst du nix rauslöschen, und natürlich will man, wenn man einen Knoten falsch gesetzt hat, den auch jederzeit wieder wegmachen können.
    Hallo ErfinderDesRades:
    ich bin nicht der Meinung, das das Zeichnen in Bitmaps ein Holzeweg ist, der Tip von FreakJNS ist goldrichtig.
    Das Bild aus Post#4 ist ein kleines Detailbildchen aus meinem vb6 Programm, das ich gerade versuche auf vb.net umzustellen. Der Ausschnitt zeigt vielleicht 5% eines normalen Projektes und auch nur wenige Details, da kommt eigentlich noch mehr. Dahinter liegt auch noch ein Autorouter, das alles funktioniert in vb6 in Echtzeit. In vb6 waren die Pictureboxen wesentlich einfacher zu handhaben, da konnte man direkt drin zeichnen und auch Koordinatensysteme einstellen, nichts was nervt oder Zeit kostet.

    Zum Verständnis:
    Ich benutze die Pictureboxen als reine Zeichenflächen, wie ein Blatt Papier, alle Aktionen des Benutzers (wo steht die Maus, welches Teil wird dadurch selektiert, wo wird gezeichnet,...) erfolgt alleine im Code durch die Mauskoordinaten und die Click- oder Move-Events, sowie die in Arrays organisierten Zeichnungselemente. Die Darstellung (wie im Beispiel) erfolgt mit dem Brute-Force Ansatz, wenn sich irgendetwas verändert, einfach alles komplett Neuzeichnen. Das habe ich in vb.net mit dem Zeichnen in Bitmaps ausprobiert, Ergebnis ca. 100.000 Elemte pro Sekunde, das reicht mir dicke.

    Und hier liegt auch mein Problem:
    Dank dem Tip von FreakJNS habe ich das Neuzeichnen im Griff, das funktioniert gut in einer Bitmap und ich habe alle Zeichenelemente die ich brauche (Linien, Kreise, Rechtecke, Texte)
    Vom Programmablauf brauche ich aber zwei Bitmaps mit identischem Inhalt, das Original und ein Backup.
    Ich will nun:
    1) das Original neuzeichnen (das funktioniert)
    2) das Original 1:1 ins Backup-BMP kopieren (das kriege ich einfach nicht hin)
    3) temporär mit dem Backup-BMP weiterarbeiten, bis das Original neugezeichnet wird, dann gehts von vorne los.

    Es muß doch eine Funktion geben, ein Bitmap 1:1 in ein anderes zu kopieren und wahlweise mit PictureBox.Image = Bitmap1 (oder halt Bitmap2) darzustellen, mehr will ich doch gar nicht ?(
    Jetzt bin ich fast da, wo ich hinwill aber zwei Probleme habe ich noch, speziell mit der Methodik das Bitmap zu kopieren.
    Ich habe mal ein Beispiel erstellt:

    1) Erst werden in Bitmap1 ein Paar Linien gemalt und auf PictureBox1 (pb1) gelegt
    2) dann wird im Move Event von PictureBox2 (pb2) das Bitmap1 in Bitmap2 kopiert (per New Bitmap, siehe Code)
    3) danach wird am Mauszeiger in Bitmap2 ein Kreis gezeichnet
    4) Als letztes wird das Graphics Object von Bitmap 2 wieder freigegeben

    Das sieht so aus:


    Und so der Code:

    VB.NET-Quellcode

    1. Private Sub PB2_MouseMove(sender As Object, e As System.Windows.Forms.MouseEventArgs) Handles PB2.MouseMove
    2. Dim BMP2 As Bitmap = New Bitmap(BMP1)
    3. Dim BMPGraphics2 As Graphics = Graphics.FromImage(BMP2)
    4. BMPGraphics2.FillEllipse(New SolidBrush(Color.Red), e.X - 20, e.Y - 20, 40, 40)
    5. PB2.Image = BMP2
    6. PB1.Image = BMP1
    7. 'BMP2.Dispose()
    8. BMPGraphics2.Dispose()
    9. End Sub


    Das Vorgehen für das kopieren des Original Bitmaps habe ich auf einer Microsoft HowTo Seite gefunden, aber dieses Vorgehen hat eine Schwachstelle: Bei jedem Move Event wird bei 2) ein neues Bitmap angelegt, was mir schnell den Speicher vollmüllt.
    Ich habe zwar ein BMPGraphics.Dispose() im Code, aber ich kann kein BMP2.Dispose() einfügen, sonst ist das neue Bild direkt wieder futsch.

    Eine Alternative habe ich auf msdn.microsoft.com/de-de/library/ms172505(v=vs.80).aspx gefunden (Gewusst wie: Kopieren von Bildern), das habe ich auch ausprobiert, aber da habe ich ein anderes Problem mit der überladenen OnPaint-Methode.
    Wie beschrieben kümmert sich der "Garbage Collector" (was ist das überhaupt?) darum, die jedesmal neu angelegten Bitmaps zu entfernen, mein Speicherverbrauch fährt auf jeden Fall nicht hoch, aber das OnPaint wird irgendwie nur sporadisch ausgelöst und ich kapier nicht wirklich wann.
    Ich kann das OnPaint auch nicht gezielt auslösen, ideal wäre es für mich, wenn ich es aus dem MouseMove Event antriggern könnte.

    Alle Methodiken die ich bisher ausprobiert habe haben aber den Nachteil, das das kopieren des Bitmap (ca. 2000x2000 Pixel) sehr langsam ist, ich schätze ca. 0,5s, das ruckelt tierisch, in meinem Beispiel oben springt der rote Kreis über den Bildschirm, wenn ich die Maus bewege.

    In vb6 ging das mit PictureBox2.Picture = PictureBox1.Image rasend schnell (PictureBox1 war visible, PictureBox2 war nicht sichtbar), das lief auf meinem alten Rechner bei max 1/3 der jetzigen Rechneleistung mindestens um den Faktor 5 schneller.

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Marcus Gräfe“ ()

    @TE: Da haste recht, wenn du meinst, dasses nicht sehr nett zu Performance und Resourcen ist, in jedem MouseMove eine 2000*2000 - bitmap umzukopieren.

    Mir sieht das aus, als seist du genau an dem Problem, ein "Control mit beweglicher Figur" (hier: kleine rote Kreisfläche) darzustellen - gugge post#2
    so, ich melde mich auch mal wieder^^
    also mit der bitmap-gschichte muss ich dem erfinderdesrades recht geben! zeichne besser direkt mittels GDI auf ein control, nicht auf eine bitmap. Auf bitmaps zeichnen ist eigentlich nur brauchbar, wenn man das ganze später als abilddatei exportieren will..
    ich bin mir nicht sicher wie sich das bei dir verhält. dein Projekt sieht nach einem straßenplan/u-bahn-netz etc aus. wenn du nun eine neue gleise hinzufügst verändert sich grafisch nur der teil, indem die neue gleise verlegt wird - der rest bleibt gleich. du kannst also den bereich der neugezeichnet werden soll begrenzen - graphics.setclip ist hier der tipp^^ das ganze sollte dann recht schnell werden UND immer aktuell sein.

    um mit den gesetzten objekten zu interagieren, also etwas zu veränden/löschen oder neu zu setzten kannst du die mauskoordinaten mit deiner datenstruktur abgleichen - ein treffer kann mit der oben beschriebenen methode schnell gehighleighted werden. andere informationen wie z.b. der rote kreis aus post #13 lassen sich so auch "einblenden"

    und als tipp zum kopieren von bitmaps:
    bitmapCopy = new bitmap(bitmapOriginal)
    Ich sehe keinen overload, der eine bitmap akzeptiert ...


    aber eine überladung erfordert ein image. image und bitmap funktionieren beide - trotz option strict on!

    Edit: wenn man sich die Bitmap-klasse im Objectbrowser anschaut sieht man, dass sie von dem image-klasse abgeleitet ist. Darum kann man der new-methode der bitmap auch eine bitmap (=> spzielles image) übergeben^^

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „FreakJNS“ ()

    picoflop schrieb:

    Und was ist mit Lockbits und Marshal.Copy?
    2000x2000x4 -> 16.000.000. Also rund 16 MB. Mit Marshal.Copy dürfte das so rund ... 1 ms ? dauern ...
    Der Tip war spitze, das geht genau in die richtige Richtung.
    Ich habe das Beispiel von der msdn Seite mal an mein Programm angepasst, so sieht der Code aus:

    VB.NET-Quellcode

    1. Private Sub PB2_MouseMove(sender As Object, e As System.Windows.Forms.MouseEventArgs) Handles PB2.MouseMove
    2. ' Lock the bitmap's bits.
    3. Dim rect As New Rectangle(0, 0, BMP1.Width, BMP1.Height)
    4. Dim bmpData1 As System.Drawing.Imaging.BitmapData = BMP1.LockBits(rect, _
    5. Drawing.Imaging.ImageLockMode.ReadWrite, BMP1.PixelFormat)
    6. Dim bmpData2 As System.Drawing.Imaging.BitmapData = BMP2.LockBits(rect, _
    7. Drawing.Imaging.ImageLockMode.ReadWrite, BMP2.PixelFormat)
    8. ' Get the address of the first line.
    9. Dim ptr1 As IntPtr = bmpData1.Scan0
    10. Dim ptr2 As IntPtr = bmpData2.Scan0
    11. ' Declare an array to hold the bytes of the bitmap.
    12. ' This code is specific to a bitmap with 24 bits per pixels.
    13. Dim bytes As Integer = Math.Abs(bmpData1.Stride) * BMP1.Height
    14. Dim rgbValues(bytes - 1) As Byte
    15. ' Copy the RGB values into the array.
    16. System.Runtime.InteropServices.Marshal.Copy(ptr1, rgbValues, 0, bytes)
    17. 'System.Runtime.InteropServices.Marshal.Copy(ptr1, ptr2, 0, bytes)
    18. 'Copy the RGB values back to the bitmap
    19. System.Runtime.InteropServices.Marshal.Copy(rgbValues, 0, ptr2, bytes)
    20. ' Unlock the bits.
    21. BMP1.UnlockBits(bmpData1)
    22. BMP2.UnlockBits(bmpData2)
    23. BMPgraphics2.FillEllipse(New SolidBrush(Color.Red), e.X - 20, e.Y - 20, 40, 40)
    24. PB2.Image = BMP2
    25. PB1.Image = BMP1
    26. End Sub


    Jetzt würde ich noch gerne den Zwischenschritt in Zeile 20+24 loswerden und den Speicherinhalt von BMP1 nicht erst in ein Array kopieren um es dann direkt wieder in Speicherinhalt von Bitmap2 zu schieben.
    Laut msdn gibt es "Copy(IntPtr,IntPtr(), Int32, Int32)", das habe ich in Zeile 21 versucht, aber da bekomme ich immer einen Fehler bei der Überladungsauflösung, was wohl am zweiten Parameter intPtr() liegt, ich weiß nicht genau wie ich das PointerArray angebe.
    Hast du mal das versucht:
    dim dest() As IntPtr = {ptr2}

    Also einfach ein dummy Array mit genau einem Element?

    Und dann Marshal.Copy(ptr1, dest, 0, length) ?

    EDIT: Klappt wohl nicht. Der kopiert so weit ich sehe, den Inhalt von Src ALS Intptr in ein Array.

    Na ja, das Problem ist halt, dass "Marshal" dafür gedacht ist, zwischen unmanaged und managed zu vermitteln. Hier gibts aber auf beiden Seiten nur unmanaged!
    In dem Fall würde ich den Zugriff auf die Windows API empfehlen:
    pinvoke.net/default.aspx/kernel32.movememory

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „picoflop“ ()