Building a Windows.Forms + GLControl based application

This tutorial assumes familiarity with Windows.Forms application development in Visual Studio 2005/C#, and at least basic knowledge of OpenGL. It also assumes a top-to-bottom readthrough; it is a guide and not a reference.

To begin with, it is quite a different approach one has to take when designing a game/application using the GLControl in a Windows.Form compared to using the GameWindow. GLControl is more low-level than GameWindow so you'll have to pull the strings on for example time measurements by yourself. In GameWindow, you get more for free!

Just as in the GameWindow case, GLControl uses the default OpenGL driver of the system, so with the right drivers installed it will be hardware accelerated. However, with large windows it will be slower than the corresponding fullscreen GameWindow, because of how the underlying windowing system works [someone with more detailed knowledge than me may want to elaborate on this..].

If you come from a "main-loop-background" (C/SDL/Allegro etc.) when it comes to coding games, you'll have to rethink that fundamentally. You'll have to change into a mindset of "what event should I hook into, and what events should I trigger, and when?" instead.

Why use Windows.Forms+GLControl instead of GameWindow?
The first thing you'll have to decide is:

"Do I really need the added complexity of Windows.Forms + embedded GLControl compared to a windowed GameWindow?"

Here are some reasons why you would like to add that complexity:

  1. You want to build a GUI-rich/RAD kind of application using the Windows.Forms controls. Eg. level editors or model viewers/editors may be in this category while a windowed game leans more towards a GameWindow kind of application.
  2. You want to have an embedded OpenGL rendering panel inside an already existing Windows.Forms application.
  3. You want to be able to do drag-and-drop into the rendering panel, for example dropping a model file into a model viewer.

Assuming you've got at least one of those reasons to build a Windows.Forms+GLControl based application, here's the steps, gotchas and whys for you.

Adding the GLControl to your Windows.Form
(I am assuming you are using Visual Studio 2005 Express Edition. Your mileage may vary if using VS2008 or Monodevelop -- I don't know the details for them -- but the follow sections should be the same no matter how you add the GLControl)
To begin with, create a Form on which you will place your GLControl. Right click in some empty space of the Toolbox, pick "Choose Items..." and browse for OpenTK.GLControl.dll. Make sure you can find the "GLControl" listed in the ".NET Framework Components", as in the image below.

Then you can add the GLControl to your form as any .NET control. A GLControl named glControl1 will be added to your Form.

Order of creation
The fact that glControl1's GLContext is created in runtime is important to remember however, since you cannot access or change glControl1's properties reliably until the GLContext has been created. The same is true for any GL.* commands (or Glu for that matter!). The conceptual order is this:

  1. First the Windows.Form constructor runs. Don't touch glControl/GL
  2. Then the Load event of the form is triggered. Don't touch glControl/GL
  3. Then the Load event of the GLControl is triggered. OK to touch glControl/GL
  4. After the Load event handler has run, any event handler may touch glControl/GL.

So one approach to address this problem is having a bool loaded=false; member variable in your Form, which is set to true in the Load event handler of the GLControl:

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

Note: the GLControl.Load event is never fired -- please use the Form.Load event instead until this issue of OpenTK is fixed.

Then in any event handler where you are going to access glControl1/GL you can put a guard first of all:

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

A popular way of adding a Load event handler to a Form is via the Properties window of Visual Studio, something like this:

  1. Click anywhere on the GLControl in the Designer
  2. Make sure glControl1 is listed in the Properties window
  3. Click the "Events" button to list all events of glControl1
  4. Double-click the empty cell right of the Load event to create and hook an event handler for the Load event

Hello World!
The absolutely minimal code you can add at this stage to see something is adding an event handler to the Paint event of glControl1 and filling it with this:

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

Yes! A beige viewport. Notice that the GLControl provides a color- and a depth buffer, which we have to clear using GL.Clear(). [TODO: how to control which buffers and formats the GLControl has? Possible at all?]

Next thing would be setting the clear color. An appropriate place to do GL initialization is in the form's Load event handler:

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

Viewport initialization
Next thing we want to do is draw a single yellow triangle.

First we need to be good OpenGL citizen and setup an orthographic projection matrix using GL.Ortho(). We need to call GL.Viewport() also.

For now we'll add this in the Load event handler by the other initialization code -- ignoring the fact that we may want to allow the user to resize the window/GLControl. We'll look into window resizing later.

I put the viewport initialization in a separate method to make it a little more readable.

     private void glControl1_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
    }

And between Clear() and SwapBuffers() our yellow triangle:

     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!

Keyboard input
Next thing we want to do is animate the triangle via user interaction. Every time Space is pressed, we want the triangle to move one pixel right.

The two general approaches to keyboard input in a GLControl scenario is using Windows.Forms key events and using the OpenTK KeyboardDevice. Since the rest of our program resides in the Windows.Forms world (our window might be a very small part of a large GUI) we'll play nice and use Windows.Forms key events in this guide.

We'll have an int x=0; variable that we'll increment in a KeyDown event handler. Adding it to the glControl1 and not the Form, means the glControl has to be focused, ie. clicked by the user for key events to be sent to our handler.

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

We add GL.Translate() to our 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
 
      ...
    }

.. and we run our program. But wait! Nothing happens when we push Space! The reason is, glControl1 is not painted all the time; the operating systems window manager (Windows/X/OSX) makes sure as few Paint events as possible happens. Only on resize, obscured window and a couple of more situations do Paint events actually get triggered.

What we would like to do is have a way of telling the window manager "This control needs to be repainted since the data it relies on has changed". We want to notify the window manager that our glControl1 should be repainted. Easy, Invalidate() to the resque:

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

Focus behaviour
If you're anything like me you're wondering a little how this focusing behaves; let's find out!

A simple way is painting the triangle yellow when glControl1 is focused and blue when it is not. Right:

     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);
 
      ...
    }

So now anytime the triangle is yellow, Space should work alright, and when it's blue any keyboard input will be ignored.

Freedom at last: resizing the window
We will now turn our attention to a sore teeth: window resizing.

Anytime a Windows.Forms control is resized, it's Resize event is triggered. That is true for glControl1 too. That's one piece in the puzzle.

The other piece to find is "What do we need to update when a GLControl is resized?" and the answer is "The viewport and the projection matrix".

Well it seems our SetupViewport() will come in handy once more! Add an event handler to the Resize event of glControl1 and fill it in:

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

There is still one problem though: if you shrink the window using eg. the bottom-right resize grip of the window, no repaint will trigger automatically. That's because the window manager makes assumptions about where the (0, 0) pixel of a control resides, namely in the top-left corner of the control. (Try resizing using the top-left grip instead - the triangle is repainted continously!). Our general fix to alleviate this problem is instructing the window manager that we really want the repaint to occur upon any resize event:

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

I want my main loop: driving animation using Application.Idle
So, what if we wanted our triangle to rotate continously? This would be childs play in a main loop scenario: just increment a rotation variable in the main loop, before we render the triangle.

But we don't have any loop. We only have events!

To mend for this lack of continuity we have to force Windows.Forms to do it our way. We want an event triggered every now and then, fast enough to get that realtime interactive feeling.

Now there are several ways to achieve this. One is adding a Timer control to our form, changing rotation in the timers Tick event handler. Another is adding a wild Thread to the soup. The first is a little too high-level and slow while the second is really low-level and a bit harder to get right.

We will take a third path and use a Windows.Forms event designed just for the purpose of being executed "when nothing else is going on": meet the Application.Idle event.

This event is special in a number of ways as you may have guessed already. It is not associated with any Form or other control, but with the program as a whole. You cannot hook into it from the GUI Designer; you'll have to add it manually -- for example in the Load event:

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

One good thing about the Idle event is that the corresponding event handlers are executed on the Windows.Forms thread. That is good since it means we can access all GUI controls without having to worry about threading issues, a pain we would have to deal with if we cooked our own thread.

So we simply increment our rotation variable in the Idle event handler and Invalidate() glControl1 -- business as usual.

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

Let's update our rendering code while we're at it:

    private void Render()
    {
      ...
      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);
      ...
    }
 
    private void glControl1_Paint(object sender, PaintEventArgs e)
    {
        Render();
    }

Happy wonders! Look:

The triangle rotates slower when the window is big! How come?
(This might not be true if you have a super-fast-computer with a super-fast-graphics-card; but you want your game to run on your neighbours computer too, don't you?)

The reason is that windowed 3d rendering just is a lot slower than full-screen rendering, in general.

But you can reduce the damage by using a technique called frame-rate independent animation. The idea is simple: increment the rotation variable not with 1 but with an amount that depends on the current rendering speed (if the speed is slow, increment rotation with a larger amount than if the speed is high).

But you need to be able to measure the current rendering speed or, equivalently, how long time it takes to render a frame.

Since .NET2.0 there is a class available to do high-precision time measurements called Stopwatch. Here's how to use it:

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

(don't try with DateTime.Now -- it has a granularity of 10 or more milliseconds, which is in the same size as typical frame rendering -- worthless..)

Now, if we could measure the time it takes to perform the glControl painting, we would be close to making some kind of frame-rate-independent animation. But there is an even more elegant way: let's measure all time that is not Application.Idle time! Then we'll be sure it is not just the painting that is measured, but everything that has been going on since our last Idle run:

     Stopwatch sw = new Stopwatch(); // available to all event handlers
    private void glControl1_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();
    }

Cool! The triangle spins with the same speed regardless of window size.

I want an FPS counter!
Yeah me too. It's quite simple now that we've got a Stopwatch.

The idea is just counting the Idle runs, and every second or so, update a Label control with the counter! But we'll have to know when a second has passed, so we need an accumulator variable adding all time slices together.

Quite a lot of logic started to add up in the Idle event handler, so I split it up a little:

     void Application_Idle(object sender, EventArgs e)
    {
      double milliseconds = ComputeTimeSlice();
      Accumulate(milliseconds);
      Animate(milliseconds);
    }
 
    private double ComputeTimeSlice()
    {
      sw.Stop();
      double timeslice = sw.Elapsed.TotalMilliseconds;
      sw.Reset();
      sw.Start();
      return timeslice;
    }
 
    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!
      }
    }

Our FPS counter in all its glory:

Can't you put the complete source code somewhere?
Sure, here it is:

 using System;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL;
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 glControl1_Load(object sender, EventArgs e)
    {
      loaded = true;
      GL.ClearColor(Color.SkyBlue); // Yey! .NET Colors can be used directly!
      SetupViewport();
      Application.Idle += 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();
    }
  }
}

Next step: What about multiple GLControls at once?
Yeah it is possible. It is even simple!

All you have to do is "make the appropriate GLControl current".

Let's say you have one GLControl named glCtrl1 and one named glCtrl2. And you have added handlers to the Paint event of both. This is what you do in the event handler of glCtrl1 (of course you do something similar in the event handler of glCtrl2!):

     private void glCtrl1_Paint(object sender, PaintEventArgs e)
    {
      if (!loaded)
        return;
 
      glCtrl1.MakeCurrent(); // Ohh.. It's that simple?
 
      GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
      ...
     }

The same is true for any code calling a GL.* or Glu.* method!

Although each GLControl has its own GraphicsContext, OpenTK will share OpenGL resources by default. This means, any context can use textures, display lists, etc. created on any other context. You can disable this behavior via the GraphicsContext.ShareContexts property.


Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
iliak's picture

At the end, you say :

"Technically, each GLControl has it's own "OpenGL context and OpenGL state" -- so for example the clear color may be different in the controls/contexts - and if you want to use the same texture in both you'll have to upload it twice!"

It's false, with OpenGL, one shares contexts with wglShareLists().

the Fiddler's picture

Thanks. OpenTK 0.9.0+ will share resources by default - I have updated the page to reflect that.

objarni's picture

Thanks!

One question: wglShareLists() does seem to be about Display Lists, according to your link.

What about textures, clear color and other GL state?

Update: re-read Fiddlers clarification in the end. But the ShareContexts flag, when is the user supposed to set it to false? Is it ok to toggle the flag at any time / in any thread of the application? A little example would be great, or at least a whole sentence.

the Fiddler's picture

Agreed, a book page about this would be nice. Will see to it.

The gist is that you can use it at any time and it only affects newly created contexts. The thread should not matter as it only sets an internal flag.

Also, wglShareLists shares everything, not only lists (it was created in a time when texture objects weren't even available). The same is true on the Linux and MacOS side, too.

Edit: you should also be aware that sharing is not always possible. For example, you cannot share data between contexts with different renderers (e.g. microsoft's GDI renderer and a hardware accelerated one) or when the visual/pixel format of the contexts is different.

objarni's picture

Thanks.

1. So after a context has been created, it's locked to a specific resource "pool"? A pool which it might share with other contexts, and which may get extended with more contexts, to but not shrunk?

2. Is it at all possible to have two different renderers in the same process in OpenTK?

the Fiddler's picture

1. Yes and no. There is a single resource pool and contexts either belong to it or not. You cannot create a second pool, but the pool itself can be shrunk by destroying contexts.

I cannot think of any use case for >1 resource pools (or context chains as we used to call them). Even simple resource sharing is considered arcane, so no need to overcomplicate things!

2. Yes.

There's nothing stopping you from creating a second context that cannot be hardware accelerated (by requesting requesting an indexed pixel format for example), but that's not really useful. It is not possible, however, to have two contexts accelerated by different video cards. I haven't tested this, mind you, but it is said this is a driver/OS limitation.

objarni's picture

1. "Yes and no. There is a single resource pool and contexts either belong to it or not. You cannot create a second pool, but the pool itself can be shrunk by destroying contexts. I cannot think of any use case for >1 resource pools (or context chains as we used to call them). 99% of times you'll want to share contexts anyway, so that's the default option."

I'm not sure I'm following you here. If there is only one resource pool, what happens in this case:

a) glCtrl1 created in designer
b) on Form_Load, ShareContexts is set to false and a new GLControl is instancianted and put somewhere on the Form (dynamically).
c) texture1 is uploaded to glCtrl1
d) texture2 is uploaded to the new GLControl.

If there is only one resource pool, where is texture2 stored?

the Fiddler's picture

Ah, I misunderstood what you meant by "resource pool". In my post, I should have said "context pool" or "context chain".

Every context holds its own resources. In your example, texture1 belongs glCtrl1 and texture2 to glCtrl2. If the contexts were shared, both textures would belong to both contexts.

One could say that there OpenTK supports only one shared resource pool (used by all shared contexts). Non-shared contexts of course contain their own distinct resource pools.

objarni's picture

One could say that there OpenTK supports only one shared resource pool (used by all shared contexts).

So if I understand you correct:

a) While SharedContexts is true two GLControls are created dynamically, A and B
b) .. then SharedContexts is set to false an a third GLControl is created dynamically, C
c) Again SharedContexts is set to true, and a fourth GLControl is created, D.

.. Then A,B,D would share textures etc while context C would have its own separate resource pool?

Technologically speaking, how much space is reserved for each resource pool? I guess it is on the gfx-card-memory, is it fixed or changing dynamically?

the Fiddler's picture

Yes, that's the idea.

Resource allocation is performed by the driver and, unless you are a driver developer or a high-profile customer, you can only guess. We know that the resources are created dynamically and allocated lazily (i.e. on first use); we also know that OpenGL resources are backed by system memory; last, we know that drivers provide (or rely on) complex memory managers and resource allocators to keep the hardware well fed with data.

I guess there is a lot to learn by studying the mesa3d and intel's linux driver implementations, but that's not exactly bedtime reading material.

Edit: Note that you can allocate more than the total amount of video memory - the driver will take care of swapping data in/out. (Of course, if your running memory needs exceed the video memory, performance will fall way down).

There's also a trend towards virtualizing video memory at the OS level, much like the main memory is virtualized. I'm not fully aware of the implications this will have on drivers, but it's one of those things deemed important for further development.