7 september 2011

Tekenen op componenten en het Paint-event

Typisch (en voor de eenvoud) wordt het maken van tekeningen in .NET als volgt aangeleerd:

  • Zet een PictureBox control op een Form
  • In een event handler (bv. een Button) maak je een Graphics object aan via deze PictureBox (met de methode CreateGraphics)
  • Teken met dit Graphics object

Dit stukje code illustreert deze aanpak:

private void drawButton_Click(object sender, EventArgs e)
{
    Graphics g = redPictureBox.CreateGraphics();
    Pen pen = new Pen(Color.Black);
    int diameter = redPictureBox.Width - 4;
    g.DrawEllipse(pen, 2, 2, diameter, diameter);
}

Dit ziet er dan zo uit:

badpicturebox

Om beginnende programmeurs snel grafische leuke toepassingen te laten maken is dit perfect, maar toch zijn er een aantal ernstige tekortkomingen:

  1. De PictureBox wordt niet bijgewerkt, bijvoorbeeld bij het minimaliseren en herstellen van de applicatie
  2. Het Graphics object g wordt niet opgeruimd achter de schermen. Volgens de MSDN documentatie moet je ervoor zorgen dat een aanroep van CreateGraphics later gevolgd wordt door een Dispose van het aangemaakte object
  3. Threading issues: de teken-code wordt uitgevoerd in de thread die verantwoordelijk is voor eventhandling. Er is echter een aparte thread die ervoor zorgt dat alle controls netjes getekend en up-to-date worden gehouden. Je mag in feite deze twee zaken niet mengen
  4. Verkeerd gebruik van PictureBox. De klassenaam zegt het zelf: een Picture Box dient om afbeeldingen te tonen en niet om allerhande lijnen en vormen op te tekenen. Hiervoor gebruik je volgens deze referentie beter een Panel.

We gaan nu deze tekortkomingen één voor één wegwerken door ons programma aan te passen.

Up-to-date houden van de PictureBox (of een Component in het algemeen)

We passen het programma als volgt aan:

private bool clicked = false;

public Form2()
{
    InitializeComponent();
}

private void drawButton_Click(object sender, EventArgs e)
{
    clicked = true;
    redPictureBox.Invalidate();
}

private void redPictureBox_Paint(object sender, PaintEventArgs e)
{
    if (clicked)
    {
        Graphics g = e.Graphics;
        Pen pen = new Pen(Color.Black);
        int diameter = redPictureBox.Width - 4;
        g.DrawEllipse(pen, 2, 2, diameter, diameter);
    }
}

Elke component (dus ook een PictureBox) wordt op gezette tijden ververst. Het besturingssysteem vraagt dan om, bijvoorbeeld na het minimaliseren en herstellen van een venster, om de componenten opnieuw te tekenen. Je kan hierop inspelen door het Paint-even af te handelen. In ons voorbeeld doen we dit als volgt:

private void redPictureBox_Paint(object sender, PaintEventArgs e)

Merk op dat we nu niet meer het Graphics object aanmaken via CreateGraphics, maar dat je het Graphics object kan verkrijgen als property van het PaintEventArgs-object e. Meer nog: CreateGraphics werkt niet in deze methode!

In de event handler van de Button geven we een signaal dat de klik heeft plaatsgevonden en dat de cirkel op de PictureBox mag worden getekend. Via Invalidate zorg je ervoor dat het Paint-event zal afgevuurd worden.

Threading issues

Als gevolg van deze aanpassing is er geen “teken”-code die uitgevoerd wordt in de event-thread:

  • Het Paint-event wordt afgehandeld in de thread die verantwoordelijk is voor het up-to-date houden van componenten
  • Invalidate wordt uitgevoerd in de event-thread, maar vraagt (onrechtstreeks) dat het Paint-event wordt afgevuurd zodat de cirkel kan worden getekend.

Panel in plaats van PictureBox

Het is een eenvoudige oefening om het voorbeeld om te bouwen. Verwijder de PictureBox en vervang door een Panel component.

Referenties:

Geen opmerkingen:

Een reactie plaatsen