Δημιουργία μιας Windows.Forms+GLControl εφαρμογής

This page is the Greek Translation of Building a Windows.Forms+GLControl based application

Αυτό το εγχειρίδιο προϋποθέτει κάποια οικειότητα με την εφαρμογή Windows.Forms σε Visual Studio 2005/C#, και τουλάχιστον βασική γνώση της OpenGL.

Για αρχή, είναι αρκετά διαφορετική η προσέγγιση που πρέπει να έχει κάποιος όταν σχεδιάζει ένα παιχνίδι/εφαρμογή χρησιμοποιώντας το GLControl σε μια Windows.Form, απ’ ότι όταν χρησιμοποιεί το GameWindow. Το GLControl είναι πιο χαμηλού επιπέδου επομένως πρέπει να κάνετε πολλά πράγματα μόνοι σας, όπως για παράδειγμα τις μετρήσεις χρόνου. Στο GameWindow έχετε περισσότερες ευκολίες!

Αν έρχεστε από ένα "main-loop-background" (C/SDL/Allegro etc.) θα πρέπει να σκεφτείτε τα πάντα από την αρχή στο προγραμματισμό παιχνιδιών. Θα πρέπει να αλλάξετε τρόπο σκέψης με κάτι σαν: «Σε ποιο event πρέπει να κολλήσω, και σε ποια να δώσω trigger, και πότε;».

Γιατί λοιπόν να χρησιμοποιήσετε Windows.Forms+GLControl αντί για GameWindow?

Το πρώτο πράγμα για το οποίο πρέπει να αποφασίσετε είναι το εξής:
"Πράγματι χρειάζομαι την επιπρόσθετη πολυπλοκότητα των Windows.Forms και του embedded GLControl αντί για ένα απλό GameWindow?"

Να μερικοί λόγοι γιατί πιθανόν να θέλατε αυτή την επιπρόσθετη πολυπλοκότητα:

  1. Θέλετε να φτιάξετε ένα GUI-rich/RAD είδος εφαρμογής χρησιμοποιώντας Windows.Forms controls πχ level editors model viewers/editors μπορεί να ανήκουν σε αυτή τη κατηγορία ενώ ένα windowed παιχνίδι είναι πιο κοντά σε μία GameWindow εφαρμογή.
  2. Θέλετε να έχετε ένα ενσωματομένο OpenGL rendering panel μέσα σε μία ήδη υπάρχουσα Windows.Forms εφαρμογή.
  3. Θέλετε να έχετε τη δυνατότητα να κάνετε drag-and-drop στο rendering panel, για παράδειγμα να τοποθετήσετε ένα μοντέλο αρχείου σε ένα μοντέλο viewer.

Υποθέτοντας λοιπόν ότι έχετε τουλάχιστον ένα από αυτούς τους λόγους για να θέλετε να φτιάξετε μία Windows.Forms+GLControl – βασισμένη εφαρμογή, να τα βήματα για να τη φτιάξετε:

Προσθέτοντας το GLControl στη Windows.Form σας

Υποθέτω ότι χρησιμοποιείτε Visual Studio 2005 Express Edition. Η διαδικασία μπορεί να διαφέρει αν χρησιμοποιείτε VS2008 or Monodevelop – δε ξέρω τις λεπτομέρειες για αυτά – αλλά οι επόμενες ενότητες θα πρέπει να είναι οι ίδιες με όποιο τρόπο και να προσθέσετε το GLControl.

Για αρχή, δημιουργήστε μία Form στην οποία θα τοποθετήσετε το GLControl σας. Κάντε δεξί κλικ σε κάποιο άδειο σημείο του Toolbox, διαλέξτε "Choose Items..." και ψάξτε το OpenTK.dll. Σιγουρευτείτε ότι μπορείτε να βρείτε το "GLControl" στη λίστα ".NET Framework Components", όπως στην παρακάτω εικόνα:

Στη συνέχεια μπορείτε να προσθέσετε το GLContrοl στη φόρμα σας σαν οποιοδήποτε .ΝΕΤ control και ένα GLControl με όνομα

glControl1<code> θα προστεθεί στη <code>Form<code> σας.
 
Το πρώτο πράγμα που θα παρατηρήσετε είναι τα άχρηστα γραφικά μέσα στο <code>glControl1

. Βασικά, το GLControl χρησιμοποιεί ένα GLContext, όπως λέγεται για να κάνει το πραγματικό GL-rendering και ούτω καθ’ εξής, και αυτό το περιεχόμενο δημιουργείται μόνο στην ώρα εκτέλεσης, όχι σχεδίασης. Οπότε μην ανησυχείτε.

Σειρά δημιουργίας

Είναι σημαντικό να το θυμάστε ότι το GLContext του glControl1 δημιουργείται κατά την εκτέλεση, εφόσον δε μπορείτε να έχετε πρόσβαση ή να αλλάξετε τις ιδιότητες του glControl1 μέχρι να δημιουργηθεί το GLCοntext. Το ίδιο ισχύει και για οποιαδήποτε GL.* εντολή (ή Glu για αυτό το θέμα!). Η σειρά σύλληψης είναι η εξής:

  1. Πρώτα τρέχει ο Windows.Form constructor. Μην πειράζετε το glControl/GL.
  2. Στη συνέχεια γίνεται trigger στο Load event της φόρμας. Μπορείτε να πειράξετε το glControl/GL.
  3. Αφού τρέξει ο event handler, οποιοσδήποτε event handler μπορεί να επηρεάσει το glControl/GL.

Επομένως μία προσέγγιση για να λύσετε αυτό το πρόβλημα είναι να έχετε μία λογική μεταβλητή bool loaded = false, μέλος στη φόρμα σας, η οποία θα γίνει true κατά το Load του event handler:

   using OpenTK.OpenGL;
  using OpenTK.OpenGL.Enums;
 
  public partial class Form1 : Form
  {
    bool loaded = false;
 
    public Form1()
    {
      InitializeComponent();
    }
 
    private void Form1_Load(object sender, EventArgs e)
    {
      loaded = true;
    }
  }

Κατόπιν σε οποιονδήποτε event handler θα έχει πρόσβαση το glControl1/GL μπορείτε να προσθέσετε τον εξής έλεγχο:

     private void glControl1_Resize(object sender, EventArgs e)
    {
      if (!loaded)
        return;
    }

Hello World!

Ο απλούστερος κώδικας που μπορείτε να προσθέσετε σε αυτό το στάδιο για να δείτε κάποιο αποτέλεσμα είναι να προσθέσετε έναν event handler στο Paint event του glControl1 και σε αυτό να γράψετε τα εξής:

     private void glControl1_Paint(object sender, PaintEventArgs e)
    {
      if (!loaded) // Play nice
        return;
 
      GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
      glControl1.SwapBuffers();
    }

Ναι! Ένα μαύρο παράθυρο! Παρατηρήστε ότι το GLControl παρέχει ένα χρώμα - και ένα buffer που πρέπει να καθαρίζουμε χρησιμοποιώντας GL.Clear(). [TODO: Πώς γίνεται να ελέγχουμε ποιους buffers και formats έχει το GLControl; Είναι δυνατόν;]

Στη συνέχεια θα μπορούσαμε να ρυθμίσουμε το καθαρό χρώμα. Το κατάλληλο μέρος για να κάνουμε την αρχικοποίηση GL είναι στον Load event handler της φόρμας:

     private void Form1_Load(object sender, EventArgs e)
    {
      loaded = true;
      GL.ClearColor(Color.SkyBlue); // Yey! .NET Colors can be used directly!
    }

Αρχικοποίηση του Viewport

Το επόμενο πράγμα που μπορούμε να κάνουμε είναι να σχεδιάσουμε ένα κίτρινο τρίγωνο.

Πρώτα χρειάζεται να είστε καλός γνώστης της OpenGL και να μπορείτε να στήσετε ένα ορθογώνιο πίνακα προβολής χρησιμοποιώντας τη GL.Ortho(). Χρειάζεται επίσης να καλέσετε τη GL.Viewport().

Προς το παρόν θα προσθέσουμε αυτό στον Load event handler από τον άλλο κώδικα αρχικοποίησης – αγνοώντας το γεγονός ότι μπορεί να θέλουμε να επιτρέψουμε στο χρήστη να αλλάζει το μέγεθος του παραθύρου/GLContrl. Θα ασχοληθούμε με το θέμα αλλαγής μεγέθους παραθύρου αργότερα.

Εδώ έβαλα τη viewport initialization σε μία ξεχωριστή μέθοδο για την κάνω λίγο πιο ευανάγνωστη.

     private void Form1_Load(object sender, EventArgs e)
    {
      loaded = true;
      GL.ClearColor(Color.SkyBlue);
      SetupViewport();
    }
 
    private void SetupViewport()
    {
      int w = glControl1.Width;
      int h = glControl1.Height;
      GL.MatrixMode(MatrixMode.Projection);
      GL.LoadIdentity();
      GL.Ortho(0, w, 0, h, -1, 1); // Bottom-left corner pixel has coordinate (0, 0)
      GL.Viewport(0, 0, w, h); // Use all of the glControl painting area
    }

Kαι μεταξύ των Clear() και SwapBuffers() το κίτρινο τρίγωνό μας:

     private void glControl1_Paint(object sender, PaintEventArgs e)
    {
      if (!loaded)
        return;
 
      GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
 
      GL.MatrixMode(MatrixMode.Modelview);
      GL.LoadIdentity();
      GL.Color3(Color.Yellow);
      GL.Begin(BeginMode.Triangles);
      GL.Vertex2(10, 20);
      GL.Vertex2(100, 20);
      GL.Vertex2(100, 50);
      GL.End();
 
      glControl1.SwapBuffers();
    }

Voila!

Είσοδος από το πληκτρολόγιο

Το επόμενο πράγμα που θέλουμε είναι η δυνατότητα στο χρήστη να κινεί το τρίγωνο. Κάθε φορά που πιέζει το Space, θέλουμε το τρίγωνο να κινείται κατά ένα pixel δεξιά.

Οι δύο γενικές προσεγγίσεις στην είσοδο από πληκτρολόγιο σε ένα GLControl σενάριο είναι να χρησιμοποιούμε τα Windows.Forms key events και την OpenTK KeyboardDevice. Εφόσον το υπόλοιπο από το πρόγραμμά μας έγκειται στον κόσμο του Windows.Forms (το παράθυρό μας μπορεί να είναι ένα μικρό κομμάτι ενός μεγαλύτερου GUI), θα παίξουμε ωραία με τα Windows.Forms key events σε αυτό τον οδηγό.

Θα έχουμε μια μεταβλητή int x=0; την οποία θα μειώνουμε με έναν KeyDown event handler. Αν αυτά τα προσθέσουμε στο glControl1 και όχι στη Form, σημαίνει ότι πρέπει να εστιάσουμε στο glControl, για παράδειγμα ο χρήστης πρέπει να κάνει κλικ για να σταλθούν τα key events στον handler μας.

     int x = 0;
    private void glControl1_KeyDown(object sender, KeyEventArgs e)
    {
      if (e.KeyCode == Keys.Space)
        x++;
    }

Προσθέτουμε την εντολή GL.Translate() στον Paint event handler:

     private void glControl1_Paint(object sender, PaintEventArgs e)
    {
      if (!loaded)
        return;
 
      GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
 
      GL.MatrixMode(MatrixMode.Modelview);
      GL.LoadIdentity();
 
      GL.Translate(x, 0, 0); // position triangle according to our x variable
 
      ...
    }

...και τρέχουμε το πρόγραμμά μας. Αλλά μισό λεπτό! Τίποτα δε συμβαίνει όταν πιέζουμε το Space! Ο λόγος είναι ότι το glControl1 δε χρωματίζεται όλη την ώρα, ο window manager των λειτουργικών συστημάτων (Windows/X/OSX) προσπαθεί να εκτελέσει όσα λιγότερα Paint events γίνεται. Μόνο στην αλλαγή μεγέθους και σε λίγες ακόμα καταστάσεις ενεργοποιούνται τα Paint events.

Επομένως, αυτό που θα θέλαμε είναι να είχαμε ένα τρόπο να πούμε στον window manager «Αυτό το control χρειάζεται να χρωματίζεται όταν αλλάζουν τα δεδομένα στα οποία βασίζεται». Θέλουμε δηλαδή να ειδοποιούμε τον window manager ότι το glControl1 πρέπει να επαναχρωματιστεί. Εύκολο, με την εντολή Invalidate():

     private void glControl1_KeyDown(object sender, KeyEventArgs e)
    {
      if (!loaded)
        return;
      if (e.KeyCode == Keys.Space)
      {
        x++;
        glControl1.Invalidate();
      }
    }

Συμπεριφορά του Focus

Αν μου μοιάζετε λίγο θα αναρωτιέστε τώρα πώς δουλεύει αυτό το focus, ας μάθουμε!
Ένας απλός τρόπος είναι να χρωματίσετε το τρίγωνο κίτρινο όταν το glControl1 είναι σε focus και μπλε όταν δεν είναι. Κάτι τετοιο δηλαδή:

     private void glControl1_Paint(object sender, PaintEventArgs e)
    {
      ...
 
      if (glControl1.Focused) // Simple enough :)
        GL.Color3(Color.Yellow);
      else
        GL.Color3(Color.Blue);
      GL.Begin(BeginMode.Triangles);
 
      ...
    }

Οπότε όποια στιγμή το τρίγωνο είναι κίτρινο, το πλήκτρο Space πρέπει να δουλεύει, και όταν είναι μπλε όποιο πλήκτρο και να πατηθεί θα πρέπει να αγνοείται.

Επιτέλους Ελευθερία: Αλλάζοντας το μέγεθος του παραθύρου

Τώρα θα ασχοληθούμε με ένα πιο σοβαρό ζήτημα: την αλλαγή μεγέθους των παραθύρων.

Όποια στιγμή ένα Windows.Forms control αλλάζει μέγεθος γίνεται trigger στο Resize event. Αυτό ισχύει και για το glControl1. Ένα το κρατούμενο.

Το επόμενο βήμα είναι να βρούμε "Τι χρειάζεται να κάνουμε για να ενημερώνουμε όταν ένα GLControl αλλάζει μέγεθος;" και η απάντηση είναι "Το viewport και τον πίνακα προβολών"

Απ' ότι φαίνεται ότι θα χρησιμοποιήσουμε το SetupViewport() ακόμα μία φορά! Προσθέστε έναν event handler στο Resize event of glControl1 και συμπληρώστε τον:

     private void glControl1_Resize(object sender, EventArgs e)
    {
      SetupViewport();
    }

Όμως υπάρχει ακόμα ένα πρόβλημα: αν μειώσετε το μέγεθος του παραθύρου μετακινώντας για παράδειγμα το κάτω-αριστερά άκρο του, δε θα γίνει αυτόματα trigger στον επαναχρωματισμό. Αυτό γίνεται γιατί ο window manager υποθέτει που βρίσκεται το (0, 0) pixel του control, ονομαστικά στην επάνω αριστερά γωνία του control. (δοκιμάστε να αλλάξετε μέγεθος μετακινώντας την πάνω αριστερά γωνία – το τρίγωνο αλλάζει συνεχώς χρώμα!) Αυτό που μπορούμε να κάνουμε για να το διορθώσουμε αυτό είναι να δώσουμε οδηγίες στον window manager ότι θέλουμε να γίνεται ο επαναχρωματισμός με κάθε event αλλαγής μεγέθους:

     private void glControl1_Resize(object sender, EventArgs e)
    {
      SetupViewport();
      glControl1.Invalidate();
    }

Θέλω τον κυρίως βρόχο μου: κίνηση με χρήση της Application.Idle

Τι θα συνέβαινε αν θέλαμε το τρίγωνό μας να περιστρεφόταν συνεχώς; Αυτό θα ήταν παιχνιδάκι σε ένα σενάριο με main loop: απλά θα αυξάναμε μία μεταβλητή rotation στην main loop, πριν σχεδιαστεί το τρίγωνο.
Όμως δεν έχουμε κανένα βρόχο, μόνο events!

Για να επιδιορθώσουμε αυτή την έλλειψη συνέχειας πρέπει να αναγκάσουμε τα Windows.Forms να το κάνουν με τον τρόπο που θέλουμε, δηλαδή να γίνεται trigger σε ένα event κάθε τρεις και λίγο, αρκετά συχνά όμως για να έχουμε την αίσθηση ότι γίνεται σε realtime.

Υπάρχουν αρκετοί τρόποι για να το πετύχουμε αυτό. Ένας είναι να προσθέσουμε ένα Timer control στη φόρμα μας, αλλάζοντας τη rotation στο Tick του event handler. Ένας άλλος είναι να προσθέσουμε και ένα Thread στο παιχνίδι. Ο πρώτος είναι πολύ υψηλού επιπέδου και αργός ενώ ο δεύτερος πολύ χαμηλού επιπέδου και λίγο πιο δύσκολο να δουλέψει σωστά.

Οπότε θα ακολουθήσουμε ένα τρίτο δρόμο και θα χρησιμοποιήσουμε ένα Windows.Forms event σχεδιασμένο να εκτελείται ειδικά για την περίπτωση όπου «τίποτ’ άλλο δε συμβαίνει», δηλαδή το Application.Idle event.
Όπως μπορεί ήδη να μαντέψατε το event αυτό είναι ιδιαίτερο με πολλούς τρόπους. Δε σχετίζεται με καμία φόρμα ή άλλο control, αλλά με το πρόγραμμα σαν σύνολο. Δε μπορείτε να το προσθέσετε από το GUI Designer, αλλά με το χέρι – για παράδειγμα, στο Load event:

     private void Form1_Load(object sender, EventArgs e)
    {
      loaded = true;
      GL.ClearColor(Color.SkyBlue);
      SetupViewport();
      Application.Idle += new EventHandler(Application_Idle); // press TAB twice after +=
    }
 
    void Application_Idle(object sender, EventArgs e)
    {
    }

Ένα καλό με το Idle event είναι ότι οι αντίστοιχοι event handlers εκτελούνται στο the Windows.Forms thread. Αυτό είναι καλό γιατί σημαίνει ότι μπορούμε να έχουμε πρόσβαση σε όλα τα GUI controls χωρίς να χρειάζεται να ανησυχούμε για θέματα των threads, ένας μπελάς που θα είχαμε να αντιμετωπίσουμε αν φτιάχναμε δικό μας thread.
Επομένως απλά αυξάνουμε τη rotation μεταβλητή στον Idle event handler και Invalidate() στο glControl1 – κατά τα γνωστά.

     float rotation = 0;
    void Application_Idle(object sender, EventArgs e)
    {
      // no guard needed -- we hooked into the event in Load handler
      rotation += 1;
      glControl1.Invalidate();
    }

Ας ενημερώσουμε και τον άλλο κώδικά μας:

     private void glControl1_Paint(object sender, PaintEventArgs e)
    {
      ...
      if (glControl1.Focused)
        GL.Color3(Color.Yellow);
      else
        GL.Color3(Color.Blue);
      GL.Rotate(rotation, Vector3.UnitZ); // OpenTK has this nice Vector3 class!
      GL.Begin(BeginMode.Triangles);
      ...
    }

Απολαύστε το!

Το τρίγωνο περιστρέφεται πιο αργά όταν το παράθυρο είναι μεγάλο! Πώς γίνεται;

Αυτό μπορεί και να μην ισχύει αν έχετε ένα πολύ γρήγορο υπολογιστή με μία πολύ γρήγορη κάρτα γραφικών, αλλά θέλετε το παιχνίδι σας να τρέχει και σε άλλους υπολογιστές, έτσι δεν είναι;

Ο λόγος είναι ότι γενικά η απεικόνιση 3d γραφικών σε παράθυρο είναι πολύ πιο αργή απ’ ότι σε πλήρη οθόνη.

Αλλά μπορείτε να μειώσετε το κακό χρησιμοποιώντας μία τεχνική που λέγεται
frame-rate independent animation. Η ιδέα είναι απλή: η αύξηση της μεταβλητής rotation όχι κατά 1 αλλά κατά μία ποσότητα που εξαρτάται από τη τρέχουσα ταχύτητα απεικόνισης (αν η ταχύτητα είναι αργή, αύξηση της rotation κατά μία μεγαλύτερη ποσότητα απ’ όταν η ταχύτητα είναι μεγάλη.

Αλλά πρέπει να μπορείτε να μετρήσετε τη τρέχουσα ταχύτητα απεικόνισης, ή ισοδύναμα, την ώρα που απαιτείται για να απεικονιστεί ένα frame.

Από τη .NET2.0 υπάρχει μία κλάση διαθέσιμη για να κάνουμε μετρήσεις υψηλής ακρίβειας που λέγεται Stopwatch. Να πώς μπορείτε να τη χρησιμοποιήσετε:

     Stopwatch sw = new Stopwatch();
    sw.Start();
    MyAdvancedAlgorithm();
    sw.Stop();
    double milliseconds = sw.Elapsed.TotalMilliseconds;

(μη το δοκιμάσετε με την DateTime.Now -- έχει σφάλμα 10 ή και περισσότερα milliseconds, δηλαδή ίδιας τάξης μεγέθους με μία τυπική frame rendering – μάταιος κόπος..)

Τώρα,αν θα μπορούσαμε να μετρήσουμε το χρόνο που χρειαζόμαστε για να κάνουμε τον glControl χρωματισμό, θα ήταν ένα πρώτο βήμα για να φτιάξουμε ένα είδος κίνησης ανεξάρτητης από το frame-rate. Αλλά υπάρχει ένας ακόμα πιο κομψός τρόπος: μπορούμε να μετρήσουμε όλο το χρόνο που δεν είναι Application.Idle χρόνος! Έτσι θα είμαστε σίγουροι ότι δε μετράμε μόνο το χρωματισμό, αλλά ό,τι συμβαίνει από το τελευταίο Idle τρέξιμο:

     Stopwatch sw = new Stopwatch(); // available to all event handlers
    private void Form1_Load(object sender, EventArgs e)
    {
      ...
      sw.Start(); // start at application boot
    }
 
    float rotation = 0;
    void Application_Idle(object sender, EventArgs e)
    {
      // no guard needed -- we hooked into the event in Load handler
 
      sw.Stop(); // we've measured everything since last Idle run
      double milliseconds = sw.Elapsed.TotalMilliseconds;
      sw.Reset(); // reset stopwatch
      sw.Start(); // restart stopwatch
 
      // increase rotation by an amount proportional to the
      // total time since last Idle run
      float deltaRotation = (float)milliseconds / 20.0f;
      rotation += deltaRotation;
 
      glControl1.Invalidate();
    }

Τέλεια! Το τρίγωνο περιστρέφεται με την ίδια ταχύτητα ανεξάρτητα από το μέγεθος του παραθύρου.

Θέλω ένα FPS μετρητή!

Ναι, κι εγώ. Είναι αρκετά απλό τώρα που έχουμε ένα Stopwatch.

Η ιδέα είναι απλά να μετράμε τα Idle τρεξίματα, και κάθε δευτερόλεπτο περίπου να ενημερώνουμε ένα Label control με τον μετρητή! Αλλά θα πρέπει να ξέρουμε πότε πέρασε το ένα δευτερόλεπτο, οπότε χρειαζόμαστε μία ακόμα μεταβληρή αθροιστική, που να προσθέτει όλες τις χρονικές περιόδους μαζί.

Έχουν μαζευτεί πολλά στον Idle event handler, οπότε θα τον χωρίσω λίγο:

     void Application_Idle(object sender, EventArgs e)
    {
      double milliseconds = ComputeTimeSlice();
      Accumulate(milliseconds);
      Animate(milliseconds);
    }
 
    float rotation = 0;
    private void Animate(double milliseconds)
    {
      float deltaRotation = (float)milliseconds / 20.0f;
      rotation += deltaRotation;
      glControl1.Invalidate();
    }
 
    double accumulator = 0;
    int idleCounter = 0;
    private void Accumulate(double milliseconds)
    {
      idleCounter++;
      accumulator += milliseconds;
      if (accumulator > 1000)
      {
        label1.Text = idleCounter.ToString();
        accumulator -= 1000;
        idleCounter = 0; // don't forget to reset the counter!
      }
    }

Ο FPS μετρητής μας σε όλη του τη δόξα:

Δε μπορώ να μαζέψω τον ολοκληρωμένο κώδικα κάπου;

Φυσικά, να τος:

 using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using OpenTK.OpenGL;
using OpenTK.OpenGL.Enums;
using OpenTK.Math;
using System.Diagnostics;
 
namespace GLControlApp
{
  public partial class Form1 : Form
  {
    bool loaded = false;
 
    public Form1()
    {
      InitializeComponent();
    }
 
    Stopwatch sw = new Stopwatch(); // available to all event handlers
    private void Form1_Load(object sender, EventArgs e)
    {
      loaded = true;
      GL.ClearColor(Color.SkyBlue); // Yey! .NET Colors can be used directly!
      SetupViewport();
      Application.Idle += new EventHandler(Application_Idle); // press TAB twice after +=
      sw.Start(); // start at application boot
    }
 
    void Application_Idle(object sender, EventArgs e)
    {
      double milliseconds = ComputeTimeSlice();
      Accumulate(milliseconds);
      Animate(milliseconds);
    }
 
    float rotation = 0;
    private void Animate(double milliseconds)
    {
      float deltaRotation = (float)milliseconds / 20.0f;
      rotation += deltaRotation;
      glControl1.Invalidate();
    }
 
    double accumulator = 0;
    int idleCounter = 0;
    private void Accumulate(double milliseconds)
    {
      idleCounter++;
      accumulator += milliseconds;
      if (accumulator > 1000)
      {
        label1.Text = idleCounter.ToString();
        accumulator -= 1000;
        idleCounter = 0; // don't forget to reset the counter!
      }
    }
 
    private double ComputeTimeSlice()
    {
      sw.Stop();
      double timeslice = sw.Elapsed.TotalMilliseconds;
      sw.Reset();
      sw.Start();
      return timeslice;
    }
 
    private void SetupViewport()
    {
      int w = glControl1.Width;
      int h = glControl1.Height;
      GL.MatrixMode(MatrixMode.Projection);
      GL.LoadIdentity();
      GL.Ortho(0, w, 0, h, -1, 1); // Bottom-left corner pixel has coordinate (0, 0)
      GL.Viewport(0, 0, w, h); // Use all of the glControl painting area
    }
 
    private void glControl1_Paint(object sender, PaintEventArgs e)
    {
      if (!loaded)
        return;
 
      GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
 
      GL.MatrixMode(MatrixMode.Modelview);
      GL.LoadIdentity();
 
      GL.Translate(x, 0, 0);
 
      if (glControl1.Focused)
        GL.Color3(Color.Yellow);
      else
        GL.Color3(Color.Blue);
      GL.Rotate(rotation, Vector3.UnitZ); // OpenTK has this nice Vector3 class!
      GL.Begin(BeginMode.Triangles);
      GL.Vertex2(10, 20);
      GL.Vertex2(100, 20);
      GL.Vertex2(100, 50);
      GL.End();
 
      glControl1.SwapBuffers();
    }
 
    int x = 0;
    private void glControl1_KeyDown(object sender, KeyEventArgs e)
    {
      if (e.KeyCode == Keys.Space)
      {
        x++;
        glControl1.Invalidate();
      }
    }
 
    private void glControl1_Resize(object sender, EventArgs e)
    {
      SetupViewport();
      glControl1.Invalidate();
    }
  }
}