Unterschiedliche Positionen in der Ausgabe einer im Canvas-Element enthaltenen Bitmap ...

renz

Neues Mitglied

Unterschiedliche Positionen in der Ausgabe einer im Canvas-Element enthaltenen Bitmap nach der Speicherung des Inhalts eines Canvas-Elements als Bitmap in einer WPF-Anwendung, wenn dem Canvas zuvor ein Visual hinzugefügt worden ist.​

VS 2019; C#; WPF

Nach meinem bisherigen Verständnis sollte einer Büchse auch nur das entnommen werden können, was zuvor in irgendeiner Weise hinein gepackt worden ist. Im vorliegenden Fall scheint jedoch etwas mehr hineinzukommen - etwas, das sich auf den Umraum der Büchse bezieht. Zur Aufgabenstellung:
  1. Einem Grid wurde in Grid.Column="5" Grid.Row="1" Grid.RowSpan="3" ein Canvas-Element hinzugefügt, das wiederum ein Image-Element sowie ein Rectangle-Element enthält. Damit die automatische Größenanpassung an den Inhalt des Image-Elements zwischen <Window> <ScrollViewer> <DockPanel> <Menu> <Grid> <Canvas> <Image> und <Rectangle jederzeit gegeben ist, wurden Breite und Höhe des Canvas-Elements an die aktuellen Werte des Images-Elements gebunden sowie die Werte für Breite und Höhe des Image-Elements auf „Auto“ festgelegt. Außerdem wurden die Werte für minimale Breite und minimale Höhe des Image-Elements an die aktuellen Werte des Canvas-Elements gebunden. Analog erfolgte die Bindung für das Rectangle-Element.
  2. Zur Laufzeit wird dem Canvas-Element eine Bitmap zugewiesen, auf der entsprechend den Gegebenheiten i.d.R. einige Tausend grafische Elemente zu erstellen sind, was u.a. mit dem >System.Windows.Shapes.Line-Befehl< bewerkstelligt werden kann.
  3. Natürlich soll die im Canvas-Element erzeugte Visualisierung auch in anderen Bildverarbeitungsprogrammen zur Verfügung stehen, so daß die Speicherung seines Inhalts als Bitmap erforderlich ist. Dem Inhalt der Bitmap liegen 3 mögliche Ansichten des Canvas-Elements zugrunde:
    1. die in das Image-Element geladene Bitmap
    2. die auf dem Image erstellte Zeichnung
    3. a und b gemeinsam in einer Ansicht.
  4. Die erforderliche Methode zum Speichern der jeweiligen Zustände folgt dem bekannten Mechanismus durch Erzeugung eines FileStream’s mit gewünschter BitmapEncoder-Auswahl. Die einzig interessante Zeile:
// Daten ausgeben:​
rtb.Render(visual: Canv01);​
// oder alternativ: rtb.Render(visual: Grd01.Children[13]);​
bestimmt, daß der Inhalt von Canvas als Visual zu rendern ist.​

Obwohl es aus meiner Sicht am bestehenden Programm nichts zu beanstanden gibt, überrascht mich doch das Ergebnis der gespeicherten Bitmap. Wie in den beigefügten Abbildungen zu sehen, wird der Canvas-Inhalt mit einem Versatz sowohl in der X- als auch der Y-Achse gespeichert, der zuvor im Code jedoch nicht explizit definiert worden war. Logischer Weise reicht nun die für die Bitmap vorgesehene Fläche, die ja nur gleich der Fläche des Canvas-Elementes sein sollte, nicht mehr aus, so daß der Canvas-Inhalt am Ende abgeschnitten erscheint. Zur Erzeugung eines optisch akzeptablen Zustands bedarf es daher der Einführung eines Korrekturwertes, der sich aus dem Abständen des Canvas-Elements zum linken und oberen Rand des Grids bestimmt. In diesem Fall sorgt die Anweisung:
Code:
[/INDENT][/INDENT]
[INDENT=2][INDENT=2]RenderTargetBitmap rtb = new RenderTargetBitmap([/INDENT][/INDENT]
[INDENT=2][INDENT=2]pixelWidth: (int)Canv01.ActualWidth + (470 * 2),[/INDENT][/INDENT]
[INDENT=2][INDENT=2]pixelHeight: (int)Canv01.ActualHeight + (30 * 2),[/INDENT][/INDENT]
[INDENT=2][INDENT=2]dpiX: 0,[/INDENT][/INDENT]
[INDENT=2][INDENT=2]dpiY: 0,[/INDENT][/INDENT]
[INDENT=2][INDENT=2]pixelFormat: PixelFormats.Pbgra32);[/INDENT][/INDENT]
[INDENT=2][INDENT=2]
für eine annehmbare „Rahmung“ des Canvas-Inhaltes. Diese kann zwar in einer Nachbearbeitung wieder entfernt werden, erscheint mir jedoch als vollkommen überflüssiger Arbeitsaufwand.

1674272405022.png

Abb. 1 – Zustand des gefüllten Canvas-Elements vor dem Speichern

1674273503440.png

Abb. 2 – Canvas-Element mit der zuvor erzeugten Bitmap

Die von mir nicht beabsichtigte Hinzufügung der Position des Canvas im Grid zur erzeugten Bitmap entspricht daher der Büchse, der mehr entnommen wurde, als hineingetan. Letztendlich handelt es sich im Programmablauf um einen Schönheitsfehler, der zu unnötigem Aufwand führt. Da mir nicht klar ist, wie dieser zu vermeiden ist, stelle ich mein Anliegen in die Community mit der Hoffnung, daß mir jemand auf die Sprünge helfen kann.
Interessanterweise entsteht beschriebener Effekt nicht, wenn in der Anweisung:
rtb.Render(visual: Canv01);​
statt des Canvas-Elements das Image-Element oder das Rectangle-Element verwendet werden. So stellt sich die Frage, wie dem Encoder beigebracht werden kann, nur das Canvas-Element im zu erzeugenden Format zu berücksichtigen.
 

Mat

Aktives Mitglied
Ist schwer zu sagen, ohne mehr vom Code zu sehen. Kann es sein, dass Bild und Zeichnungen nicht relativ zum Canvas (der Leinwand) sondern relativ zum gesamten Grid berechnet werden und daraus der Offset resultiert? Dafür könnte sprechen, dass das Problem beim direkten Speichern des Bildes nicht auftritt.

Welchen Parent haben die Bitmap und das Zeichenobjekt und wie werden sie dem Canvas zugeordnet?

XAML-Beispiel:
XML:
<Grid>
    <Canvas>
        <Image HorizontalAlignment="Left" VerticalAlignment="Top" />
    </Canvas>
</Grid>
 
Zuletzt bearbeitet:

renz

Neues Mitglied
Hallo Mat,

vielen Dank für die Zuschrift. Zunächst beweist das Ergebnis der Speicherung, daß vom Encoder absolute Positionsbestimmungen vorgenommen oder verwendet worden sind, wohingegen die Aufforderung, nur den Inhalt des Canvas zu rendern, ein relatives Bezugssystem erwarten läßt. (Spaßeshalber hatte ich in einem Test die Möglichkeiten zur RenderTransformation mit der Einstellung für >relative Werte< verwendet, um festzustellen, ob die Änderung der Renderingposition sich im Encoder-Verhalten widerspiegelt. Da dies unter Verwendung der Korrekturwerte für die Position des Canvas im Grid: <TranslateTransform X="-470" Y="-30"/> der Fall war und alle abgespeicherten Bitmaps in den drei Varianten das gewünschte Aussehen zeigen, steht zumindest fest, daß in irgendeiner Weise codegesteuerte Einflussnahme möglich ist. Leider kann die RenderTransformation nicht dauerhaft angewendet werden, da die vor dem Canvas im Grid vorhandenen Inhalte durch das Canvas-Element zur Laufzeit überdeckt werden.)
Die Ausrichtungsmerkmale waren, wie in Ihrem Text aufgeführt, eingestellt. Sie hätten aber auch auf "Stretch" eingestellt werden können, da durch die vorgenommenen Bindungen der einzelnen Steuerelemente zueinander ihre Anwendung außer Kraft gesetzt worden ist.

Anmerkung zum "direkten Speichern":
- Wird die Image-Abbildung mit der vorgesehenen Methode zur Speicherung während der Laufzeit benutzt, entsteht natürlich auch eine Bitmap mit unerwünschter Randzone.
- Nur wenn im Code >rtb.Render(visual: Canv01);< "Canv01" gegen "Rect01" oder "Img01" ausgetauscht wird, sind unterschiedliche Ergebnisse der Ansicht gegeben.
 

Mat

Aktives Mitglied
Ich hab versucht, das Programm der Beschreibung und den Screenshots entsprechend rudimentär nachzubauen:


1674542979782.png


Mit folgenden Einstellungen für den Canvas:
1674543101086.png


Dank Stretch funktioniert der Export immer einwandfrei, auch wenn der Canvas größer ist als das Bild. Die roten Markierungen (bei mir nur gefüllte Kreise) übernimmt er auch.

Es kann natürlich sein, dass Stretch aufgrund der speziellen Gridstruktur keine Option ist oder für schwarze Ränder sorgt. ZB wenn der ganze Spaß in einer Viewbox drin steckt und die schon ihre eigenen Stretch-Mechaniken hat. Dann ist zuschneiden eine Option, aber das schneidet natürlich auch Markierungen ab, die außerhalb des Bildes liegen oder nicht transformiert wurden.

Für genauere Angaben müssten wir aber schon die Funktion für das Laden und das Exportieren von Bildern sehen, denke ich. Und die Struktur des Grids kann auch nicht schaden.
 

renz

Neues Mitglied
Das mit den roten Markierungen ist schon ein wenig brutal. Ich kann also davon ausgehen, daß das Canvas-Element und nicht das Image in der Nachstellung gerendert worden ist. Bezüglich der Einstellungen für das Canvas-Element bestehen auch keine Unterschiede. Die Abfolge der beteiligten Steuerelemente wurde bereits unter Pkt. 1 meines Beitrags mitgeteilt. Aus der benannten Position des Canvas-Elements im Grid ergibt sich die Struktur des Grids (was aber unerheblich sein dürfte, da es nur um die Position des Canvas im Grid geht). Aber Grid-Struktur und Stretch-Eigenschaften haben nur insofern eine Bedeutung, als sie einen Abstand des Canvas zur Position x/Y = 0 des Fensters definieren. Im Vergleich mit einer anderen Fenster-Lösung, bei der es nur ein DockPanel, dem an der linken Seite ein StackPanel mit 2 Buttons angeheftet ist, gibt, steht der Rest der Fläche einem Canvas zur Verfügung, wobei am Ende die gespeicherte Bitmap denselben Versatz (schwarzer Rand in der Breite des StackPanels an der linken Bildseite) aufweist. In diesem Fall waren keine speziellen Bindungen unter den Steuerelementen beteiligt! Bleibt zu guter Letzt die Speicherung:

C#:
private
void BitmapSpeichern_Click(object sender, RoutedEventArgs e) {
  string verz = @"...\Grafiken\"; SaveFileDialog sfd =
      new SaveFileDialog(){Filter = "JPEG-Datei (*.JPG)|*.jpg|PNG-Datei (*.PNG)|*.png|BMP-Datei (*.BMP)|*.bmp|WMF-Datei (*.WMF)|*.wmf", InitialDirectory = verz};
  if (sfd.ShowDialog() == true) {
    // Zielbitmap entsprechend der Größe der Zeichenfläche erzeugen:
    RenderTargetBitmap rtb = new RenderTargetBitmap(pixelWidth
                                                    : (int)Canv01.ActualWidth + 940, pixelHeight
                                                    : (int)Canv01.ActualHeight + 60, dpiX : 0, dpiY : 0, pixelFormat
                                                    : PixelFormats.Pbgra32);

    // Daten ausgeben:
    rtb.Render(visual : Canv01);
    // alternativ : rtb.Render(visual: Grd01.Children[13]);

    try {
      FileStream fs = new FileStream(sfd.FileName, FileMode.Create, FileAccess.Write);
      // Mit dem JPEG - Encoder kodieren:
      if (sfd.FilterIndex == 1) {
        JpegBitmapEncoder encoder = new JpegBitmapEncoder();
        encoder.Frames.Add(item : BitmapFrame.Create(rtb));
        encoder.Save(fs);
        fs.Close();
      }
    ...

mit freundlichem Gruß
 
Oben Unten