9 november 2011

Over lijnen en nieuwe lijnen

In Visual Basic kan je op veel manieren een nieuwe lijn beginnen, bijvoorbeeld:

MessageBox.Show("Dag " + vbNewLine +
                "Meneer, " + Environment.NewLine +
                "Alles " + ControlChars.NewLine +
                "Naar wens?")

Vooral de klasse ControlChars is handig, want die bevat ook nog handige properties om tabs, formfeeds, … toe te voegen.

Wil je bovenstaand voorbeeld omzetten naar C#, dan blijven twee opties over:

MessageBox.Show("Dag \r\n" +
                "Meneer " + Environment.NewLine +
                "Alles \n" +
                "Naar wens?");

Ofwel werk je met Environment.NewLine, dan voeg je het nieuwelijn-teken toe dat voor het platform (in mijn geval Windows 7) gangbaar is. Ofwel werk je met de codes die vanuit de C-taal gegroeid zijn: \r\n is voor windowsplatformen het nieuwelijnteken voor tekstbestanden, \n is dat voor unixvarianten. Dit laatste werkt echter wel voor het voorbeeld hierboven.

En wat als je toch wil volharden en gebruik wil maken van ControlChars? Wel, dan moet je in eerste instantie de namespace Microsoft.VisualBasic toevoegen aan je C#-project. Dit kan door te rechtsklikken in je project op References en vervolgens via Add Reference het bestand Microsoft.VisualBasic.dll toe te voegen. Vervolgens importeer je deze namespace bovenaan in je bestand:

using Microsoft.VisualBasic;

En nu kan je deze klasse gebruiken in je C#-programma. Belangrijk nadeel: je moet een hele assembly (dll) toevoegen en laden in het geheugen omwille van slechts één klasse. Daarom kan je eventueel de raad uit dit artikel volgen en je eigen ControlChars maken, eventueel in een aparte namespace (bv. KrisHermans.Utils):

namespace KrisHermans.Utils
{
    public class ControlChars
    {
        public const string CrLf = "\r\n";
        public const string NewLine = Environment.NewLine;
        public const char Cr = (char)13;
        public const char Lf = (char)10;
        public const char Back = (char)8;
        public const char FormFeed = (char)12;
        public const char Tab = (char)9;
        public const char VerticalTab = (char)11;
        public const char NullChar = (char)0;
        public const char Quote = (char)34;
    }
}

8 november 2011

Tijdsduur meten van bewerkingen

Stel dat je in een programma de duur van een operatie wil meten, bijvoorbeeld:

  • hoelang het duurt om een bestand in te lezen;
  • hoelang een databasebewerking duurt;
  • hoelang het tekenen van een complexe figuur duurt;

Een voor de hand liggende aanpak zou zijn om gebruik te maken van twee instanties van DateTime en hiervan het verschil (een TimeSpan object) af te drukken, zoals:

DateTime start = DateTime.Now;
FooMethodToMeasure();
DateTime stop = DateTime.Now;
Console.WriteLine("Duur {0} msec", (stop - start).TotalMilliseconds);

Deze manier van werkwijze zou echter niet nauwkeurig zijn (bron 1, bron 2), en daarom gebruik je beter de StopWatch klasse, die rechtstreeks via het besturingssysteem een timer voorziet. Let op: je moet wel de System.Diagnostics namespace importeren.

Stopwatch watch = new Stopwatch();
watch.Start();
FooMethodToMeasure();
Console.WriteLine("Duur {0} msec", watch.ElapsedMilliseconds);
watch.Stop();

31 oktober 2011

Predikaten met C#

Predikaten (Engels: Predicate) verkorten vaak het programmeerwerk omdat ze het zelf schrijven van herhalingsstructuren overbodig maken. Ze komen handig van pas bij het zoeken. In plaats van zelf lussen te schrijven, gebruik je een ingebouwde zoekmethode zoals Array.Find of Array.FindAll en kan je via predikaten zelf je zoekcriteria “inpluggen”.

Vanuit de logica weet je wellicht dat een predikaat is een bewering is die waar of onwaar is. In C# is een predikaat een speciale methode (een delegate) die true of false oplevert. Het volgende voorbeeld is een predikaat die onderzoekt of een woord een palindroom is: achterstevoren gelezen blijft het woord hetzelfde.

public bool IsPalindroom(string woord)
{
    // omzetten naar karakterarray
    char[] karakters = woord.ToCharArray();
    // omdraaien
    Array.Reverse(karakters);
    // opnieuw naar string omzetten
    string omgekeerd = new string(karakters);
    // vergelijken
    return omgekeerd == woord;
}

Stel nu dat je een lijst van woorden hebt die je wil onderzoeken of het palindromen zijn:

private List<string> woordenLijst = new List<string>();

...

woordenLijst.Add("zeven");
woordenLijst.Add("lepel");
woordenLijst.Add("koekoek");
woordenLijst.Add("racecar");
woordenLijst.Add("racewagen");
woordenLijst.Add("topkookpot");

In plaats van zelf een for-lus te schrijven en één voor één de methode toe te passen, gebruik je de Array.FindAll-methode en voorzie je via het predikaat een zoekcriterium:

List<string> palindromen = woordenLijst.FindAll(IsPalindroom);

MessageBox.Show("gevonden palindromen: " +
                String.Join(", ", palindromen.ToArray()));

De lus is er nu nog wel, Array.FindAll doorloopt alle woorden en evalueert ze via het predikaat, en stopt de resultaten in een nieuwe lijst. De hoeveelheid programmeerwerk is echter afgenomen en je redeneert meer op een declaratieve manier: je denkt meer naar over “wat heb ik nodig (IsPalindroom)?” in plaats van “hoe moet ik dat programmeren (lussen)?”.

27 september 2011

Gratis afbeeldingen geleverd door Visual Studio

In de installatiedirectory van Visual Studio zit een .zip-bestand met daarin een boel afbeeldingen (icons) die je kan gebruiken voor je eigen programma’s. Het is zelfs toegelaten om die iconen te gebruiken voor commerciële programma’s.

Op mijn machine (Windows 7, 64 bit en Visual Studio 2010) vond ik in de map C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\VS2010ImageLibrary\1033 het bestand VS2010ImageLibrary.zip met daarin de afbeeldingen.

19 september 2011

Entiteiten van naam veranderen

Als je een methode, variabele, parameter, … van naam wil veranderen kan je natuurlijk een “search & replace”-actie over je bestanden doen. Vaak is het gemakkelijker om via rechtsklik > Refactor > Rename de naamsverandering door te voeren.

refactor

Omzetten naar gehele getallen

Er zijn meerder methoden om een double getal om te zetten naar een geheel getal. De cast-operator realiseert een getal door afkapping, terwijl Convert.ToInt32 afrondt naar het dichtsbijzijnde geheel getal.

Bijvoorbeeld:

double d = 5.6;
int a = (int)d; // a = 5
int b = Convert.ToInt32(d); // b = 6

Bij de afronding maakt men gebruik van het zogenaamde Bankiersalgoritme. Dit betekent dat, als een getal halfweg ligt tussen twee gehele getallen, je afrondt naar het dichtstbijzijnde even getal. Dus 4.5 wordt 4, en 5.5 wordt 6.

Wil je “gewoon” afronden, dien je te werken met de methode Math.Round waarbij je met een extra parameter instelt welk soort afronding je wenst.

13 september 2011

Key events op een Form

Volgend programma demonstreert hoe je kan detecteren of en welke van de pijltjestoetsen ingedrukt en weer losgelaten worden.

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_KeyDown(object sender, KeyEventArgs e)
    {
        HandleKeyEvent(e, "down");
    }

    private void Form1_KeyUp(object sender, KeyEventArgs e)
    {
        HandleKeyEvent(e, "released");
    }

    private void HandleKeyEvent(KeyEventArgs e, string state)
    {
        if (e.KeyCode == Keys.Up)
            infoLabel.Text = "Arrow up is " + state;
        if (e.KeyCode == Keys.Down)
            infoLabel.Text = "Arrow down is " + state;
        if (e.KeyCode == Keys.Left)
            infoLabel.Text = "Arrow left is " + state;
        if (e.KeyCode == Keys.Right)
            infoLabel.Text = "Arrow right is " + state;
    }
}

De events die ons hier interesseren zijn KeyDown (toets ingedrukt) en KeyUp (toets weer losgelaten). Door de property KeyCode te vergelijken met één van de voorgedefinieerde constanten in de klasse Keys, kan je exact bepalen om welke toets het gaat.

Dit geeft als uitvoer bijvoorbeeld:

KeyEvents

Bemerk ook het gebruik van een methode HandleKeyEvent om zoveel mogelijk dubbele code te vermijden.

12 september 2011

Drag & Drop in .NET programma’s

Gebaseerd op dit artikel ga ik je het meest eenvoudige programma uit de doeken doen dat drag & drop ondersteunt. De applicatie toont een leeg formulier waarop je één (of meerdere) bestanden kan “droppen”. Bijvoorbeeld vanaf de desktop of de verkenner, zoals hieronder aangegeven.

DNDTest1

Hier sleep je 3 bestanden en 1 directory naar het formulier door eerst met CTRL die items aan te klikken en vervolgens te slepen naar de centrale zone. Als je de muis vervolgens loslaat, verschijnt een bericht met alle bestands- en mapnamen:

DNDTest2

Om te beginnen start je een leeg project. De control die de drop-operaties ontvangt, moet dit toelaten: vandaar de property AllowDrop die je op true zet:

public Form1()
{
    InitializeComponent();

    AllowDrop = true;
}

Nu moet je twee events afhandelen: DragEnter en DragDrop. Het eerste event (DragEnter) wordt afgevuurd zodra de gebruiker met de muis het formulier binnengaat. Hiermee kan je de muiscursorafbeelding wijzigen indien de operatie toegelaten is:

private void Form1_DragEnter(object sender, DragEventArgs e)
{
    if (e.Data.GetDataPresent(DataFormats.FileDrop))
    {
        e.Effect = DragDropEffects.All;
    }
    else
    {
        e.Effect = DragDropEffects.None;
    }
}

De if-test (met e.Data.GetDataPresent) controleert of er wel bestanden gedropt worden. Indien dat het geval is, stel je de property e.Effect op de waarde DragDropEffects.All. Hierdoor verschijnt het bekende plusteken bij de muiscursor. In het andere geval is draggen verboden en wordt de muiscursor daaraan aangepast. Sleep bijvoorbeeld de prullenbak eens naar het formulier en je zal merken dat dit niet lukt.

Het tweede event bevat de eigenlijke gegevens (in dit geval de bestandsnamen) en handel je als volgt af:

private void Form1_DragDrop(object sender, DragEventArgs e)
{
    string[] files = (string[])e.Data.GetData(DataFormats.FileDrop, false);

    string message = String.Join("\r\n", files);
    MessageBox.Show(message);
}

Eerst zet je de data om naar een string-array, hiervoor is een cast-operatie noodzakelijk. Vervolgens formatteer je deze array tot één string en toon je die in een berichtvenster.

Download het volledige programma.

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: