Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

More about OpenGL with C/C++ and C# on ReactOS (or Windows)

5.00/5 (5 votes)
15 Mar 2021CPOL11 min read 9.7K   372  
Second step to a serious looking OpenGL application on plain old Win32 API for ReactOS (and also for Windows or WINE)
This article contains a sample application that demonstrates how to create a serious looking OpenGL application (with menu bar, toolbar, status bar and actor box) for ReactOS based on plain old Win32 API. In addition, a small library is created which can be used to write almost code-identical applications in C/C++ and C#.

Introduction

ReactOS is an open source alternative to the Windows operation system. Even if the first version of ReactOS dates back to 1998, there is still no 'stable' version of ReactOS. May be, the most important reason is a lack of attention.

All the results of this article run on Windows as well (tested on Windows 10 - 64 bit edition).

Why "Second" Step?

This article is based on the tips:

That's why I call it "the second step" to a serious looking OpenGL application.

What Is the Motivation?

I want to test the limits of ReactOS. So I have to live with the restrictions, that ReactOS imposes on me:

  1. C/C++: No Visual Studio, but the very good Code::Blocks or the easy to use Dev-C++
  2. Library: No MFC, ATL and so on, but a stable Win32 API
  3. C#: No Microsoft .NET runtime beyond version 2.0 and no Microsoft csc.exe at all, but MONO 4.0.3 including csc.exe - assemblies, compiled with it, will also run on the .NET runtime
  4. Assembly: No Windows Forms or WPF, but MONOs excellent P/Invoke

I don't think the limitations are too strong to give it a try:

  1. Code::Blocks doesn't make me miss anything. It works fine in combination with MinGW.
  2. Since MFC, ATL and so on are based on the Win32 API - what's the limitation when working without MFC, ATL and so on? My answer is: I'll miss the shorter code and the dynamic layout (of controls) only. Both can be easily achieved with thin wrappers around the Win32 API as well.
  3. MONO's csc.exe only makes me miss one thing: Resources. But embedded binary representation of images and icons can easily bridge this gap.
  4. For Windows Forms, I would say, it's the same as with MFC: The shorter code and the dynamic layout (of controls) can be easily achieved with thin wrappers around the Win32 API as well.

What Is the Objective?

In the best case: A library, which shortens the C/C++ code considerably, offers a dynamic layout of the controls and can also be used from C#. I'm thinking of C/C++ and C# simultaneously, because I can't make up my mind yet: C/C++ (fast and 100% under my control) or C# (comfortable and elegant).

Is My Library 'Yet Another OpenGL Utility'?

I don't think so, because my library has only minor overlaps with GLUT/freeglut, GLFW, GLEW, GLee, SDL, OpenTK, ... These libraries focus mainly on platform independence and optimized access to OpenGL. While the creation of an application window is also part of almost all of these libraries, these libraries are primarily designed to support full-screen applications or applications where the contents of the application's main window is used entirely as an OpenGL canvas.

My library is designed to use only a part of the contents of the application's main window as an OpenGL canvas. The OpenGL API is not wrapped at all. My library is on the one hand very rudimentary and on the other hand easily expandable. The source code of the controls is short, well commented and the concept is easily transferable to new controls.

Background

After the article Introduction to OpenGL with C/C++ on ReactOS has shown that the simplest boiler plate code of an OpenGL application runs under the above mentioned conditions, the question arises how the way to a serious OpenGL application could look like. My requirements are:

  • Typical look & feel of a desktop application with menu bar, toolbar and status bar
  • Decent controls for user interaction
  • An Open-GL window
  • Elastic resizing behavior

It could look like this on ReactOS:

Image 1

And on Windows 10 like this:

Image 2

I've created a sample application that includes three test cases to verify the compliance with my requirements:
  • One OpenGL test, using only a part of the contents of the application's main window.
  • One test for the elastic resizing behavior, using a row layout.
  • And one test for the elastic resizing behavior, using a column layout.

The sample application's Test menu can be used to switch dynamically between these three test. All controls of a completed test case, that are no longer required, are dynamically removed and all new controls, that are required for the next test case, are dynamically created.

Image 3

Image 4

Elastic row layout, buttons with default style

Image 5

Elastic column layout, buttons with flat style

This is demonstrated by the OpenGL test:

  • There is one BLANK control, that serves as an actor bar and demonstrates fixed width/dynamic height. The actor bar contains:
    • Two OgwwStatic controls, that show text, are flagged to notify the parent about events and demonstrate fixed size. The first one switches between triangle and hexagon, the second switches between red/green/blue and cyan/yellow/magenta colouring of the OpenGL animation.
    • Two OgwwStatic controls, that show icons, are flagged to notify the parent about events and demonstrate fixed size. The first one switches between clockwise and counterclockwise rotation of the OpenGL animation. The second switches between stretched and centered icon - but this works for ReactOS only.
    • Two OgwwBlank controls, that show a solid background color and demonstrate fixed size. The first one has a raised border, the second ones have a sunken border.
  • There is one OgwwBlank control, that shows the OpenGL animation and demonstrates dynamic resizing.

ATTENTION: The control, that shows OpenGL context, must be the last created one. Oherwise the OpenGL viewport calculation doesn't fit the control size.

This is demonstrated by both elastic layout tests:

  • There are two OgwwStatic controls, that show text and demonstrate dynamic resizing.
  • There are three OgwwButton controls, that demonstrate fixed size. The first button displays an image. The second image displays a different font style. The third button is the default button.
  • There is one OgwwEdit control, that demonstrates dynamic resizing.

What Do I Mean by "Elastic Layout"/Dynamic Width, Height or Resizing?

Elastic layout allows the relative positioning of the controls to each other and to keep the controls at their relative position when resizing the parent window. Controls can either resize appropriate to/proportional to the parent window (this behaviour is applied to both static text controls and the edit control within my sample application's elastic layout tests) or retain their original size and adopt the intermediate space (this behaviour is applied to all three button controls within my sample application's elastic layout tests).

Image 6

Unlike .NET's approach to realize an elastic layout for Windows Forms - where elasticity was achieved by docking and padding - my approach uses rows and cells or columns and cells. This solution is more similar to the layout managers, introduced by Java AWT and adopted by younger toolkits like GTK, wxWidgets, WPF and many more.

My approach is based on layouting rows, layouting columns and layouted cells, that can be filled with controls. It doesn't support padding (yet). Spaces must be realized by empty cells. Since empty cells do not necessarily require Windows resources, I think this is an appropriate solution.
My approach supports arbitrary nesting of layouting rows and layouting columns. The only drawback is that you have to choose the primary layout between layouting rows and layouting columns.

My library is on the one hand very rudimentary and on the other hand easily expandable. The source code of the controls is short, well commented and the concept is easily transferable to new controls. If you want to extend this concept, I recommend the articles, Automatic Layout of Resizable Dialogs, ClassLib, A C++ class library, Sizers: An Extendable Layout Management Library, Sharp Layout, or many more.

Using the Code

Conventions

On ReactOS, I use Code::Blocks with the MinGW development environment. MinGW is known for a function name decoration that is partly incompatible to the function name decoration used by Microsoft Visual Studio. The Win32 API is compiled with __stdcall calling convention and the MinGW function name decoration for __stdcall doesn't match it. So I decided to use __cdecl calling convention for my library.

                        MSVC DLL           Digital Mars       MinGW DLL
  Call Convention   |   (dllexport)    |   Compiler DLL   |   (dllexport)   |   BCC DLL
----------------------------------------------------------------------------------------------
  __stdcall         |   _Function@n    |   _Function@n    |   Function@n    |   Function
  __cdecl           |   Function       |   Function       |   Function      |   _Function

Consequently, declarations of imported functions in C/C++ look like this (and use __cdecl):

C++
/// <summary>
/// Get the current debug level name as an P/Invoke (interop) aware string,
/// that will be hand over the memory ownership to caller.
/// </summary>
/// <returns>The debug level name as an P/Invoke (interop) aware string.</returns>
/// <remarks>The caller is responsible to call Marshal.PtrToStringUni() and
/// Marshal.FreeCoTaskMem() or CoTaskMemFree().</remarks>
extern LPCWSTR __cdecl Utils_CoGetDebugLevelName();

The declarations of imported functions in C# look like this (and use CallingConvention = CallingConvention.Cdecl):

C#
/// <summary>
/// Get the current debug level name.
/// </summary>
/// <returns>The debug level name as an P/Invoke (interop) aware string.</returns>
/// <remarks>The caller is responsible for the release of the <see cref="LPTSTR"> by calls
/// to Marshal.PtrToStringUni() and Marshal.FreeCoTaskMem() or CoTaskMemFree().</remarks>
/// <remarks>Managed code interop marshalling always releases non-primitive types ("Non-
/// Blittable" types like strings). See: "https://docs.microsoft.com/en-us/dotnet/framework/
/// interop/interop-marshaling"</remarks>
[DllImport("ogww32.dll", EntryPoint = "Utils_CoGetDebugLevelName",
           CallingConvention = CallingConvention.Cdecl, CharSet=CharSet.Unicode)]
public static extern string GetDebugLevelName();

The above example, selected for demonstration, has another special feature: The return value is a "Non-Blittable" data type - a string for this case. "Non-Blittable" datatypes like strings are always automatically released by .NET. The Library API must take this into account and provide as well as accept these data types as copies (that can hand over the memory management to .NET).

My library uses WCHAR (unicode character) only - to provide maximum compatibility to .NET. There is only one exception - the ReactOS implementation of CreateWindowW doesn't process the windowName parameter correctly and I use CreateWindowA (and consequently RegisterClassA as well) instead.

The Sample Application

My MainFrame control represents the application main window. MenuBar control, ToolBar control and StatusBar control are managed by the MainFrame control automatically - means that the resizable layout is managed by the application window. My MainFrame control can handle either any control or any layouter on the remaining space. My OpenGL test, row layouter test and column layouter test register one layouter respectively to the application window's remaining space - each test provides its own layouter.

Image 7

Application Initialization I

This is how the C# code looks like to create MainFrame control, MenuBar control, ToolBar control and StatusBar control:

C#
/// <summary>
/// Start the program. GUI applications should use only one thread to manipulate controls.
/// </summary>
/// <param name="args">The command line arguments.</param>
public static void Main(string[] args)
{
    try
    {
        HINSTANCE hModule = ::GetModuleHandle(null);
        TheApplication.Singleton = new TheApplication(hModule, IntPtr.Zero);  // 0010
        TheApplication.Singleton.Run(args);                                   // 0011
                
        TheApplication.Singleton.Dispose();
        System.Threading.Thread.Sleep(3000);
        // Console.GetText();
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        System.Threading.Thread.Sleep(3000);
        // Console.GetText();
    }
}

My TheApplication class is a singleton. For convenient access, this class provides the field Singleton.

The module handle and previous module handle are necessary for the Win32 API. However, I will refrain from investigating previous module handle.

The lines 0010 und 0011 construct the application and run it. The code behind that follows here...

C#
/// <summary>
/// Initialize a new instance of the <see cref="TheApplication"/> class with instance and
/// previous instance handle.
/// </summary>
/// <param name="hInst">The application instance handle.</param>
/// <param name="hPrevInst">The previous application instance handle.</param>
TheApplication(HINSTANCE hInst, HINSTANCE hPrevInst)
{
    OgwwConsole.WriteMessageFws("Initial debug level is: %s\n", OgwwUtils.GetDebugLevelName());
    OgwwUtils.SetDebugLevel(2);
    OgwwConsole.WriteMessageFws("New debug level is: %s\n", OgwwUtils.GetDebugLevelName());
                
    _puniqueMainFrame = OgwwMainFrame.Construct(hInst, hPrevInst);
    _pweakStatusBar   = IntPtr.Zero;
    _pweakToolBar     = IntPtr.Zero;

    _pweakOglLayouter = IntPtr.Zero;
    _pweakOglCanvas   = IntPtr.Zero;

    _hDevCtx          = IntPtr.Zero;
    _hGlRc            = IntPtr.Zero;
}

Since my Win32 API wrapper library doesn't handle the MainFrame control pointer natively, it must be treated by the application as a unique pointer (_puniqueMainFrame). All other controls are natively handled by the Win32 API wrapper library and are treated by the application as weak pointers (_pweakStatusBar...). For a description of the concept of unique/weak pointers, see smart pointers.

C#
/// <summary>
/// Run the application class <see cref="TheApplication"/>
/// </summary>
/// <param name="args">The command line arguments.</param>
void TheApplication::Run(string[] args)
{
    // Extended Win32 functionality initialization.
    Win32.InitCommonControls();
    //INITCOMMONCONTROLSEX icc;
    //icc.dwSize = sizeof(icc);
    //icc.dwICC = ICC_WIN95_CLASSES/*|ICC_COOL_CLASSES|ICC_DATE_CLASSES|
    //               ICC_PAGESCROLLER_CLASS|ICC_USEREX_CLASSES*/;
    OgwwThemes.Init();

    OgwwMainFrame.RegisterMessageLoopPreprocessCallback(_puniqueMainFrame,
        new OgwwGenericWindow.WNDPROCCB(this.MainWindowMessageLoopPreprocessCallback));
    if (OgwwMainFrame.Show(_puniqueMainFrame, "MyWin", "OpenGLfromDLL", SW.SHOWDEFAULT) ==
        IntPtr.Zero)
    {
        OgwwConsole.WriteError("Window creation failed!\n");
        return;
    }

    int  result = OgwwMainFrame.Run(_puniqueMainFrame,
        new OgwwGenericWindow.IDLEPROCCB(this.MessageLoopIdleCallback));
    OgwwGenericWindow.DestroyWindow(OgwwGenericWindow.HWnd(_puniqueMainFrame));

    OgwwThemes.Release();
}

Since my library is Win32 based, the application must provide a WindowProc to handle events. I implemented this in a way that the WindowsProc of the application can decide whether the standard message processing within the library should go on processing the current message (true) or not (false) by using the return value.

Because the WindowsProc of the application is called before the WindowsProc of the library, I called it *PreprocessCallback. This is the initialization part...

C#
/// <summary>
/// Called from WindowProcedure to pre-process the current message.
/// </summary>
/// <param name="hWnd">The handle of the window, the windows event loop procedure is called
/// for.</param>
/// <param name="msg">The message, the <c>WindowProcedure</c> shall process.</param>
/// <param name="wp">The <c>WPARAM</c> parameter of the message, the <c>WindowProcedure</c>
/// shall process.</param>
/// <param name="lp">The <c>LPARAM</c> parameter of the message, the <c>WindowProcedure</c>
/// shall process.</param>
/// <returns>Returns <c>true</c> if WindowProcedure shall go on processing the current message,
/// or <c>false</c> otherwise.</returns>
public BOOL MainWindowMessageLoopPreprocessCallback(HWND hWnd, UINT msg,
                                                    IntPtr wParam, IntPtr lParam)
{
    switch (msg)
    {
        case WM.CREATE: // 1
            {
                if (TheApplication.Singleton != null)
                {
                    TheApplication.Singleton.AddMenueBar(hWnd);
                    TheApplication.Singleton.AddStatusBar(hWnd);
                    TheApplication.Singleton.AddToolBar(hWnd);
                    TheApplication.Singleton.AddOpenGlContent(hWnd);
                }
                break;
            }

        ...

    }
    return true;
}

The WindowsProc of the application initializes MenuBar, StatusBar, Toolbar and main content by calling AddMenueBar, AddStatusBar, AddToolBar and AddOpenGlContent.

C#
/// <summary>
/// Initialize the entire menu bar.
/// </summary>
/// <param name="hWnd">The handle of the parent window.</param>
private void TheApplication::AddMenuBar(HWND hWnd)
{
    HMENU hMenu     = OgwwMainFrame.CreateMenu();


    HMENU hFileMenu = OgwwMainFrame.CreateMenu();
    OgwwMainFrame.AppendMenuPopup(hMenu, hFileMenu, "&File");

    OgwwMainFrame.AppendMenuEntry(hFileMenu, MENU_FILE_NEW_ID, "&New");
    OgwwMainFrame.AppendMenuEntry(hFileMenu, MENU_FILE_OPEN_ID, "&Open");
    OgwwMainFrame.AppendMenuSeparator(hFileMenu);
    OgwwMainFrame.AppendMenuEntry(hFileMenu, MENU_FILE_EXIT_ID, "E&xit");

    HMENU hTestMenu = OgwwMainFrame.CreateMenu();
    OgwwMainFrame.AppendMenuPopup(hMenu, hTestMenu, "&Test");

    OgwwMainFrame.AppendMenuEntry(hTestMenu, MENU_TEST_CASE1_ID, "Case &1 - OpenGL");
    OgwwMainFrame.AppendMenuEntry(hTestMenu, MENU_TEST_CASE2_ID, "Case &2 - Row layout");
    OgwwMainFrame.AppendMenuEntry(hTestMenu, MENU_TEST_CASE3_ID, "Case &3 - Column layout");

    OgwwMainFrame.AppendMenuEntry(hMenu, MENU_HELP_ID, "&Help");

    OgwwMainFrame.SetMenu(hWnd, hMenu);
}

In order to be able to assign the messages in the WindowsProc of the application to the MenuBar entries, I use the IDs MENU_FILE_NEW_ID, MENU_FILE_OPEN_ID, MENU_FILE_EXIT_ID, MENU_TEST_CASE1_ID, MENU_TEST_CASE2_ID, MENU_TEST_CASE3_ID and MENU_HELP_ID.

C#
/// <summary>
/// Initialize the entire status bar.
/// </summary>
/// <param name="hWnd">The handle of the parent window.</param>
private void TheApplication::AddStatusBar(HWND hWnd)
{
    _pweakStatusBar = OgwwMainFrame.StatusBarCreateAndRegister(_puniqueMainFrame,
                                                               hWnd, STATUS_BAR_ID, 1);
    OgwwStatusBar.SetText(_pweakStatusBar, 0, "Ready for action!!!");
}

The StatusBar uses the simplest of all possible variants - with only 1 part.

C#
/// <summary>
/// Initialize the entire tool bar.
/// </summary>
/// <param name="hWnd">The handle of the parent window.</param>
private void TheApplication::AddToolBar(HWND hWnd)
{
    _pweakToolBar = OgwwMainFrame.ToolBarCreateAndRegister(_puniqueMainFrame,
                                                           hWnd, TOOL_BAR_ID, 16, 3);

    OgwwToolBar.AddButton(_pweakToolBar,
                          OgwwUtils.CreateDIBitmapFromBytes(16, 16, (WORD)1, (WORD)8,
                              BMP_NEW2_256.IMG_ColorBits(),  BMP_NEW2_256.IMG_ColorCount(),
                              BMP_NEW2_256.IMG_PixelBits(),  true),
                          OgwwUtils.CreateDIBitmapFromBytes(16, 16, (WORD)1, (WORD)1,
                              BMP_NEW2_256.MASK_ColorBits(), BMP_NEW2_256.MASK_ColorCount(),
                              BMP_NEW2_256.MASK_PixelBits(), true),
                          MENU_FILE_NEW_ID,
                          /*TBSTATE_ENABLED*/ (BYTE)4,
                          /*TBSTYLE_BUTTON*/  (BYTE)0);

    OgwwToolBar.AddButton(_pweakToolBar,
                          OgwwUtils.CreateDIBitmapFromBytes(16, 16, (WORD)1, (WORD)8,
                              BMP_OPEN2_256.IMG_ColorBits(),  BMP_OPEN2_256.IMG_ColorCount(),
                              BMP_OPEN2_256.IMG_PixelBits(), true),
                          OgwwUtils.CreateDIBitmapFromBytes(16, 16, (WORD)1, (WORD)1,
                              BMP_OPEN2_256.MASK_ColorBits(), BMP_OPEN2_256.MASK_ColorCount(),
                              BMP_OPEN2_256.MASK_PixelBits(), true),
                          MENU_FILE_OPEN_ID,
                          /*TBSTATE_ENABLED*/ (BYTE)4,
                          /*TBSTYLE_BUTTON*/  (BYTE)0);

    OgwwToolBar.AddButton(_pweakToolBar,
                          IntPtr.Zero,
                          IntPtr.Zero,
                          (UINT)0,
                          (BYTE)0,
                          /*TBSTYLE_SEP*/     (BYTE)1);

    OgwwToolBar.AddButton(_pweakToolBar,
                          OgwwUtils.CreateDIBitmapFromBytes(16, 16, (WORD)1, (WORD)8,
                              BMP_HELP_256.IMG_ColorBits(),  BMP_HELP_256.IMG_ColorCount(),
                              BMP_HELP_256.IMG_PixelBits(), true),
                          OgwwUtils.CreateDIBitmapFromBytes(16, 16, (WORD)1, (WORD)1,
                              BMP_HELP_256.MASK_ColorBits(), BMP_HELP_256.MASK_ColorCount(),
                              BMP_HELP_256.MASK_PixelBits(), true),
                          MENU_HELP_ID,
                          /*TBSTATE_ENABLED*/ (BYTE)4,
                          /*TBSTYLE_BUTTON*/  (BYTE)0);
    OgwwToolBar.Show(_pweakToolBar);
}

In order to be able to assign the messages in the WindowsProc of the application to the ToolBar entries, I re-use the menu entry IDs MENU_FILE_NEW_ID, MENU_FILE_OPEN_ID and MENU_HELP_ID.

That was pretty simple and uncomplicated.

Application Initialization II

The next step is to fill the application's window main content. I'll start with the OpenGL test - that should support elastic layout. This is my detailed design approach:

Image 8

This is the construction code for the OpenGL test:

C#
/// <summary>
/// Initialize the entire main content for the "OpenGL test".
/// </summary>
/// <param name="hWnd">The handle of the parent window.</param>
private void TheApplication::AddOpenGlContent(HWND hWnd)
{
    if (_pweakOglLayouter != IntPtr.Zero)
        return;

    _pweakOglLayouter = OgwwRowLayouter.Construct();
    OgwwMainFrame.LayouterRegister(_puniqueMainFrame, _pweakOglLayouter);

    LPVOID pweakRow = OgwwRowLayouter.AddRowVariableHeight(_pweakOglLayouter, 1.0f, 1);

    Win32.POINT p;
    Win32.SIZE s;

    p.x = 0;
    p.y = 0;
    s.cx = 100;
    s.cy = 100;

    // ATTENTION: The window, that serves as OpenGl canvas must be the last created one!
    // Later created windows will be overridden!
    LPVOID pweakActorPlane = OgwwBlank.ConstructPlane(
        OgwwGenericWindow.HInst(_puniqueMainFrame), hWnd, ACTOR_ID, p, s, false);
    _pweakOglCanvas = OgwwBlank.ConstructCanvas(
        OgwwGenericWindow.HInst(_puniqueMainFrame), hWnd, CANVAS_ID, p, s);

    OgwwLayouterRow.AddCellVariableDimension(pweakRow, "OnlyRowCanvas",
        _pweakOglCanvas, 1.0f, 60);
    OgwwLayouterRow.AddCellFixedDimension   (pweakRow, "OnlyRowActors", pweakActorPlane, 77);

    // ATTENTION: All subsequent layout is based on the 'pweakActorPlane'.
    // This widget serves as the parent for interactive controls.
    // That's why this control needs a message loop callback.
    OgwwBlank.RegisterMessageLoopPreprocessCallback(pweakActorPlane,
        new OgwwGenericWindow.WNDPROCCB(this.ActorPlaneMessageLoopPreprocessCallback));

    /* Start up OpenGL and run the window. */
    HDC   hDC = IntPtr.Zero;
    HGLRC hRC = IntPtr.Zero;

    OgwwBlank.EnableOpenGL(OgwwGenericWindow.HWnd(_pweakOglCanvas), out hDC, out hRC,
                           (BYTE)24, (BYTE)16);
    if (hDC != IntPtr.Zero && hRC != IntPtr.Zero)
        OgwwConsole.WriteInformation("OpenGL enabled.\n");
    else
        OgwwConsole.WriteError("OpenGL enabling failed!\n");

    this.SetHDevCtx(hDC);
    this.SetHGlResCtx(hRC);

    HROWLAYOUTER pweakActionLayouter = OgwwRowLayouter.Construct();
    OgwwBlank.SetLayouter(pweakActorPlane, pweakActionLayouter);

    p.x = 0;
    p.y = 0;
    s.cx = 32;
    s.cy = 24;

    OgwwRowLayouter.AddRowFixedHeight(pweakActionLayouter, 2);
    LPVOID pweakActorRow02 = OgwwRowLayouter.AddRowFixedHeight(pweakActionLayouter, s.cy);
    OgwwRowLayouter.AddRowFixedHeight(pweakActionLayouter, 1);
    LPVOID pweakActorRow04 = OgwwRowLayouter.AddRowFixedHeight(pweakActionLayouter, s.cy);
    OgwwRowLayouter.AddRowFixedHeight(pweakActionLayouter, 1);
    LPVOID pweakActorRow06 = OgwwRowLayouter.AddRowFixedHeight(pweakActionLayouter, s.cy);

    _pweakACTOR_TOOL_EDGES = OgwwStatic.ConstructLabel(
        OgwwGenericWindow.HInst(_puniqueMainFrame), OgwwGenericWindow.HWnd(pweakActorPlane),
        ACTOR_TOOL_EDGES_ID, p, s, true);
    OgwwGenericWindow.SetText(_pweakACTOR_TOOL_EDGES, ACTOR_TOOL_EDGES_LABEL0);
    _pweakACTOR_TOOL_COLORS = OgwwStatic.ConstructLabel(
        OgwwGenericWindow.HInst(_puniqueMainFrame), OgwwGenericWindow.HWnd(pweakActorPlane),
        ACTOR_TOOL_COLORS_ID, p, s, true);
    OgwwGenericWindow.SetText(_pweakACTOR_TOOL_COLORS, ACTOR_TOOL_COLORS_LABEL0);

    OgwwLayouterRow.AddCellFixedDimension   (pweakActorRow02, "UpperRowLeftSpace",
                                             IntPtr.Zero, 2);
    OgwwLayouterRow.AddCellVariableDimension(pweakActorRow02, "UpperRowLabel1",
                                             _pweakACTOR_TOOL_EDGES, 0.3f, 24);
    OgwwLayouterRow.AddCellFixedDimension   (pweakActorRow02, "UpperRowMiddleSpace",
                                             IntPtr.Zero, 2);
    OgwwLayouterRow.AddCellVariableDimension(pweakActorRow02, "UpperRowLabel2",
                                             _pweakACTOR_TOOL_COLORS, 0.3f, 24);
    OgwwLayouterRow.AddCellFixedDimension   (pweakActorRow02, "UpperRowRightSpace",
                                             IntPtr.Zero, 2);

    _pweakACTOR_TOOL_ROTATION = OgwwStatic.ConstructIcon(
        OgwwGenericWindow.HInst(_puniqueMainFrame), OgwwGenericWindow.HWnd(pweakActorPlane),
        ACTOR_TOOL_ROTATION_ID, p, s, true);
    HICON hIcon = OgwwUtils.CreateIconFromBytes(ICO_COUNTERCLOCKWISE_16.Bytes(),
        ICO_COUNTERCLOCKWISE_16.ByteCount(), 16, 16);
    OgwwStatic.SetIcon(_pweakACTOR_TOOL_ROTATION, hIcon, false);
    LPVOID _pweakACTOR_TOOL_STATICTEST = OgwwStatic.ConstructBitmap(
        OgwwGenericWindow.HInst(_puniqueMainFrame), OgwwGenericWindow.HWnd(pweakActorPlane),
        ACTOR_TOOL_STATICTEST_ID, p, s, true);
    HBITMAP hBMP = OgwwUtils.CreateDIBitmapFromBytes(16, 16, (WORD)1, (WORD)8,
        BMP_HELP_256.IMG_ColorBits(), BMP_HELP_256.IMG_ColorCount(),
        BMP_HELP_256.IMG_PixelBits(), true);
    OgwwStatic.SetBitmap(_pweakACTOR_TOOL_STATICTEST, hBMP, false);

    OgwwLayouterRow.AddCellFixedDimension(pweakActorRow04,    "SecondRowLeftSpace",
                                          IntPtr.Zero, 1);
    OgwwLayouterRow.AddCellVariableDimension(pweakActorRow04, "SecondRowLabel1",
                                          _pweakACTOR_TOOL_ROTATION, 0.3f, 24);
    OgwwLayouterRow.AddCellFixedDimension(pweakActorRow04,    "SecondRowLeftSpace",
                                          IntPtr.Zero, 1);
    OgwwLayouterRow.AddCellVariableDimension(pweakActorRow04, "SecondRowLabel1",
                                             _pweakACTOR_TOOL_STATICTEST, 0.3f, 24);
    OgwwLayouterRow.AddCellFixedDimension(pweakActorRow04,    "SecondRowRightSpace",
                                          IntPtr.Zero, 1);

    LPVOID pweakPlane01 = OgwwBlank.ConstructPlane(OgwwGenericWindow.HInst(_puniqueMainFrame),
        OgwwGenericWindow.HWnd(pweakActorPlane), ACTOR_TOOL_COLORTILE01_ID, p, s, true);
    OgwwBlank.SetBackBrush(pweakPlane01, Win32.CreateSolidBrush(0x00806060));
    OgwwBlank.SetFrameEdge(pweakPlane01, /* BDR_RAISEDOUTER */ 1);
    OgwwBlank.SetFrameFlags(pweakPlane01, /* BF_RECT */ 15);
    LPVOID pweakPlane02 = OgwwBlank.ConstructPlane(OgwwGenericWindow.HInst(_puniqueMainFrame),
        OgwwGenericWindow.HWnd(pweakActorPlane), ACTOR_TOOL_COLORTILE02_ID, p, s, true);
    OgwwBlank.SetBackBrush(pweakPlane02, Win32.CreateSolidBrush(0x00608060));
    OgwwBlank.SetFrameEdge(pweakPlane02, /* BDR_SUNKENOUTER */ 2);
    OgwwBlank.SetFrameFlags(pweakPlane02, /* BF_RECT */ 15);

    OgwwLayouterRow.AddCellFixedDimension(pweakActorRow06,    "FourthRowLeftSpace",
                                          IntPtr.Zero, 1);
    OgwwLayouterRow.AddCellVariableDimension(pweakActorRow06, "FourthRowLabel1",
                                             pweakPlane01, 0.3f, 24);
    OgwwLayouterRow.AddCellFixedDimension(pweakActorRow06,    "FourthRowLeftSpace",
                                          IntPtr.Zero, 1);
    OgwwLayouterRow.AddCellVariableDimension(pweakActorRow06, "FourthRowLabel1",
                                             pweakPlane02, 0.3f, 24);
    OgwwLayouterRow.AddCellFixedDimension(pweakActorRow06,    "FourthRowRightSpace",
                                          IntPtr.Zero, 1);

    // Ensure right size by forcing main frame content layout and an unusual old viewport size.
    Win32.SendMessage(hWnd, Win32.WM.WM_SIZE, 0, 0);
    _viewportSize.cx = 0;
    _viewportSize.cy = 0;
}

The best thing in the programmatic creation of a GUI compared to a resource-based creation of a GUI is the ability to dynamically build and deconstruct the GUI completely or partially.

This is the destruction code for the OpenGL test:

C#
/// <summary>
/// Destruction of the entire main content for the "OpenGL test".
/// </summary>
/// <param name="hWnd">The handle of the parent window.</param>
void TheApplication::RemoveOpenGlContent(HWND hWnd)
{
    if (_pweakOglLayouter == NULL)
        return;

    _pweakACTOR_TOOL_EDGES      = NULL;
    _pweakACTOR_TOOL_COLORS     = NULL;
    _pweakACTOR_TOOL_ROTATION   = NULL;
    _pweakACTOR_TOOL_STATICTEST = NULL;

    // Get ownership of the row.
    HROWLAYOUTER puniqueRow = OgwwRowLayouter::RemoveLast(_pweakOglLayouter);
    while (puniqueRow != NULL)
    {
        // Get ownership of the cell.
        HLAYOUTERCELL puniqueCell = OgwwLayouterRow::RemoveLast(puniqueRow);
        while (puniqueCell != NULL)
        {
            if (OgwwLayouterCell::Window(puniqueCell) == _pweakOglCanvas)
            {
                /* Shut down OpenGL. */
                HDC   hDC    = GetHDevCtx();
                HGLRC hRC    = GetHGlResCtx();
                OgwwBlank::DisableOpenGL(OgwwGenericWindow::HWnd(_pweakOglCanvas), hDC, hRC);
                OgwwConsole::WriteInformation(L"OpenGL disabled.\n");
                SetHDevCtx(NULL);
                SetHGlResCtx(NULL);

                _pweakOglCanvas = NULL;
            }
            else if (OgwwLayouterCell::Window(puniqueCell) != NULL)
            {
                LPVOID pweakWindow = OgwwLayouterCell::Window(puniqueCell);
                if (pweakWindow != NULL)
                {
                    LPVOID puniqueSubLayouter = OgwwBlank::GetLayouter(pweakWindow);
                    if (puniqueSubLayouter != NULL)
                    {
                        // Get ownership of the row.
                        HROWLAYOUTER puniqueSubRow =
                            OgwwRowLayouter::RemoveLast(puniqueSubLayouter);
                        while (puniqueSubRow != NULL)
                        {
                            // Get ownership of the cell.
                            HLAYOUTERCELL puniqueSubCell =
                                OgwwLayouterRow::RemoveLast(puniqueSubRow);
                            while (puniqueSubCell != NULL)
                            {
                                OgwwLayouterCell::Destruct(puniqueSubCell, true);
                                puniqueSubCell = NULL;
                                // Get ownership of the cell.
                                puniqueSubCell = OgwwLayouterRow::RemoveLast(puniqueSubRow);
                            }
                            OgwwLayouterRow::Destruct(puniqueSubRow);
                            puniqueSubRow = NULL;
                            // Get ownership of the row.
                            puniqueSubRow = OgwwRowLayouter::RemoveLast(puniqueSubLayouter);
                        }
                    }
                }
            }
            OgwwLayouterCell::Destruct(puniqueCell, true);
            puniqueCell = NULL;
            puniqueCell = OgwwLayouterRow::RemoveLast(puniqueRow);
        }
        OgwwLayouterRow::Destruct(puniqueRow);
        puniqueRow = NULL;
        // Get ownership of the row.
        puniqueRow = OgwwRowLayouter::RemoveLast(_pweakRowLayouter);
    }
    OgwwMainFrame::LayouterUnregister(_puniqueMainFrame);
    OgwwRowLayouter::Destruct(_pweakRowLayouter);
    _pweakOglLayouter = NULL;
}

Change to a Layout Test

Since the elastic layout tests with row layouter and column layouter are very similar, I'll only show the row layouter here. This is my detailed design approach:

Image 9

This is the construction code for the row layouter test:

C#
/// <summary>
/// Initialize the entire main content for the "Row layouter test".
/// </summary>
/// <param name="hWnd">The handle of the parent window.</param>
void TheApplication::AddRowLayoutContent(HWND hWnd)
{
    LONG  w1 = 100;
    LONG  h1 = 16;
    LONG  w2 = 65;
    LONG  h2 = 20;
    LONG  w3 = 210;
    LONG  h3 = 64;

    if (_pweakRowLayouter != NULL)
        return;

    _pweakRowLayouter = OgwwRowLayouter::Construct();
    OgwwMainFrame::LayouterRegister(_puniqueMainFrame, _pweakRowLayouter);

    OgwwRowLayouter::AddRowVariableHeight(_pweakRowLayouter, 0.1f, 1);
    LPVOID pweakRow02 = OgwwRowLayouter::AddRowFixedHeight(_pweakRowLayouter, h1);
    OgwwRowLayouter::AddRowFixedHeight(_pweakRowLayouter, 5);
    LPVOID pweakRow04 = OgwwRowLayouter::AddRowFixedHeight(_pweakRowLayouter, h2);
    OgwwRowLayouter::AddRowFixedHeight(_pweakRowLayouter, 5);
    LPVOID pweakRow06 = OgwwRowLayouter::AddRowVariableHeight(_pweakRowLayouter, 0.8f, h3);
    OgwwRowLayouter::AddRowVariableHeight(_pweakRowLayouter, 0.1f, 1);

    POINT p;
    SIZE  s;

    p.x   = 10;
    p.y   = 50;
    s.cx  = w1;
    s.cy  = h1;
    LPVOID pweakL1 = OgwwStatic::ConstructLabel(OgwwGenericWindow::HInst(_puniqueMainFrame),
                         hWnd, LAYOUTTEST_LABEL1_ID, p, s, false);
    OgwwGenericWindow::SetText(pweakL1, L"AABBCCyy");

    p.x   = 135;
    p.y   = 50;
    LPVOID pweakL2 = OgwwStatic::ConstructLabel(OgwwGenericWindow::HInst(_puniqueMainFrame),
                         hWnd, LAYOUTTEST_LABEL2_ID, p, s, false);
    OgwwGenericWindow::SetText(pweakL2, L"DDEEFFyy");

    OgwwLayouterRow::AddCellFixedDimension   (pweakRow02, L"UpperRowLeftSpace",   NULL,  10);
    OgwwLayouterRow::AddCellVariableDimension
                     (pweakRow02, L"UpperRowLabel1",    pweakL1, 0.3f, 60);
    OgwwLayouterRow::AddCellVariableDimension
                     (pweakRow02, L"UpperRowMiddleSpace", NULL,  0.1f, 10);
    OgwwLayouterRow::AddCellVariableDimension
                     (pweakRow02, L"UpperRowLabel2",    pweakL2, 0.3f, 60);
    OgwwLayouterRow::AddCellFixedDimension   
                     (pweakRow02, L"UpperRowRightSpace",  NULL,    10);

    p.x   = 10;
    p.y   = 74;
    s.cx  = w2;
    s.cy  = h2;
    LPVOID pweakB1 = 
      OgwwButton::ConstructBitmapButton(OgwwGenericWindow::HInst(_puniqueMainFrame),
      hWnd, LAYOUTTEST_BUTTON1_ID, p, s, false);
    OgwwGenericWindow::SetText(pweakB1, L"GHy");

    HBITMAP hBmp = OgwwUtils::CreateDIBitmapFromBytes(16, 16, (WORD)1, (WORD)8,
                                                      BMP_NEW2_256_ColorBits(),
                                                      BMP_NEW2_256_ColorCount(),
                                                      BMP_NEW2_256_PixelBits(), TRUE);
    OgwwButton::SetBitmap(pweakB1, hBmp, false);

    p.x   = 100;
    p.y   = 74;
    LPVOID pweakB2 = OgwwButton::ConstructPushButton
                     (OgwwGenericWindow::HInst(_puniqueMainFrame),
                         hWnd, LAYOUTTEST_BUTTON2_ID, p, s, false, false);
    OgwwGenericWindow::SetText(pweakB2, L"JKy");
    OgwwGenericWindow::SetFont(pweakB2, L"Courier NewMiddleRow", 11, 400);

    p.x   = 190;
    p.y   = 74;
    LPVOID pweakB3 = OgwwButton::ConstructPushButton
                     (OgwwGenericWindow::HInst(_puniqueMainFrame),
                         hWnd, LAYOUTTEST_BUTTON3_ID, p, s, false, true);
    OgwwGenericWindow::SetText(pweakB3, L"MNy");

    OgwwLayouterRow::AddCellFixedDimension   (pweakRow04, L"MiddleRowLeftSpace",  NULL, 10);
    OgwwLayouterRow::AddCellFixedDimension   (pweakRow04, L"MiddleRowLabel3",  pweakB1, s.cx);
    OgwwLayouterRow::AddCellVariableDimension
                     (pweakRow04, L"MiddleRowLeftSpace",  NULL, 0.1f, 10);
    OgwwLayouterRow::AddCellFixedDimension   (pweakRow04, L"MiddleRowLabel4",  pweakB2, s.cx);
    OgwwLayouterRow::AddCellVariableDimension
                     (pweakRow04, L"MiddleRowRightSpace", NULL, 0.1f, 10);
    OgwwLayouterRow::AddCellFixedDimension   (pweakRow04, L"MiddleRowLabel5",  pweakB3, s.cx);
    OgwwLayouterRow::AddCellFixedDimension   (pweakRow04, L"MiddleRowRightSpace", NULL, 10);

    p.x   = 10;
    p.y   = 104;
    s.cx  = w3;
    s.cy  = h3;
    LPVOID pweakE1 = OgwwEdit::Construct(OgwwGenericWindow::HInst(_puniqueMainFrame),
                         hWnd, LAYOUTTEST_EDIT_ID, p, s);
    OgwwGenericWindow::SetFont(pweakE1, L"Courier NewMiddleRow", 11, 400);

    OgwwLayouterRow::AddCellFixedDimension   (pweakRow06, L"LowerRowLeftSpace",  NULL, 10);
    OgwwLayouterRow::AddCellVariableDimension
                     (pweakRow06, L"LowerRowEdit",    pweakE1, 1.0f, s.cx);
    OgwwLayouterRow::AddCellFixedDimension   (pweakRow06, L"LowerRowRightSpace", NULL, 10);

    // Ensure right size by forcing main frame content layout 
    // and an unusual old viewport size..
    ::SendMessage(hWnd, WM_SIZE, 0, 0);
}

It's the same for the row layouter test as for the OpenGL test - there is a destruction code for the GUI:

C#
/// <summary>
/// Destruction of the entire main content for the "Row layouter test".
/// </summary>
/// <param name="hWnd">The handle of the parent window.</param>
void TheApplication::RemoveRowLayoutContent(HWND hWnd)
{
    if (_pweakRowLayouter == NULL)
        return;

    // Get ownership of the row.
    HROWLAYOUTER puniqueRow = OgwwRowLayouter::RemoveLast(_pweakRowLayouter);
    while (puniqueRow != NULL)
    {
        // Get ownership of the cell.
        HLAYOUTERCELL puniqueCell = OgwwLayouterRow::RemoveLast(puniqueRow);
        while (puniqueCell != NULL)
        {
            OgwwLayouterCell::Destruct(puniqueCell, true);
            puniqueCell = NULL;
            puniqueCell = OgwwLayouterRow::RemoveLast(puniqueRow);
        }
        OgwwLayouterRow::Destruct(puniqueRow);
        puniqueRow = NULL;
        // Get ownership of the row.
        puniqueRow = OgwwRowLayouter::RemoveLast(_pweakRowLayouter);
    }
    OgwwMainFrame::LayouterUnregister(_puniqueMainFrame);
    OgwwRowLayouter::Destruct(_pweakRowLayouter);
    _pweakRowLayouter = NULL;
}

Library Expansion

I would like to come back to the method AddOpenGlContent. Since it uses two row layouters, the layouters are interconnected by a Plane control. The Plane control must also have a WindowProc to handle the events (it's child events) too. The event handler looks like this:

C#
/// <summary>
/// Called from WindowProcedure to pre-process the current message.
/// </summary>
/// <param name="hWnd">The handle of the window, the windows event loop procedure is
/// called for.</param>
/// <param name="msg">The message, the <c>WindowProcedure</c> shall process.</param>
/// <param name="wp">The <c>WPARAM</c> parameter of the message, the <c>WindowProcedure</c>
/// shall process.</param>
/// <param name="lp">The <c>LPARAM</c> parameter of the message, the <c>WindowProcedure</c>
/// shall process.</param>
/// <returns>Returns <c>true</c> if WindowProcedure shall go on processing the current message,
/// or <c>false</c> otherwise.</returns>
bool ActorPlaneMessageLoopPreprocessCallback(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
    switch(msg)
    {
        case WM_COMMAND: // 273
        {
            switch(wp)
            {
                case ((WPARAM)ACTOR_TOOL_EDGES_ID):
                    if (_edges == 3)
                    {
                        _edges = 6;
                        if(_pweakACTOR_TOOL_EDGES != NULL)
                            OgwwGenericWindow::SetText(_pweakACTOR_TOOL_EDGES,
                                                       ACTOR_TOOL_EDGES_LABEL1);
                    }
                    else
                    {
                        _edges = 3;
                        if(_pweakACTOR_TOOL_EDGES != NULL)
                            OgwwGenericWindow::SetText(_pweakACTOR_TOOL_EDGES,
                                                       ACTOR_TOOL_EDGES_LABEL0);
                    }
                    break; // Returns true and continues processing this message.

                case ((WPARAM)ACTOR_TOOL_COLORS_ID):
                {
                    if (_colors == 0)
                    {
                        _color1.Red = 0.0f; _color1.Green = 1.0f; _color1.Blue = 1.0f;
                        _color2.Red = 1.0f; _color2.Green = 0.0f; _color2.Blue = 1.0f;
                        _color3.Red = 1.0f; _color3.Green = 1.0f; _color3.Blue = 0.0f;
                        _color4.Red = 0.5f; _color4.Green = 0.5f; _color4.Blue = 0.7f;
                        _color5.Red = 0.7f; _color5.Green = 0.5f; _color5.Blue = 0.5f;
                        _color6.Red = 0.5f; _color6.Green = 0.7f; _color6.Blue = 0.7f;

                        if(_pweakACTOR_TOOL_COLORS != NULL)
                            OgwwGenericWindow::SetText(_pweakACTOR_TOOL_COLORS,
                                                       ACTOR_TOOL_COLORS_LABEL1);
                        _colors = 1;
                    }
                    else
                    {
                        _color1.Red = 1.0f; _color1.Green = 0.0f; _color1.Blue = 0.0f;
                        _color2.Red = 0.0f; _color2.Green = 1.0f; _color2.Blue = 0.0f;
                        _color3.Red = 0.0f; _color3.Green = 0.0f; _color3.Blue = 1.0f;
                        _color4.Red = 0.7f; _color4.Green = 0.7f; _color4.Blue = 0.0f;
                        _color5.Red = 0.0f; _color5.Green = 0.7f; _color5.Blue = 0.7f;
                        _color6.Red = 0.7f; _color6.Green = 0.0f; _color6.Blue = 0.7f;

                        if(_pweakACTOR_TOOL_COLORS != NULL)
                            OgwwGenericWindow::SetText(_pweakACTOR_TOOL_COLORS,
                                                       ACTOR_TOOL_COLORS_LABEL0);
                        _colors = 0;
                    }
                    break; // Returns true and continues processing this message.
                }
                case ((WPARAM)ACTOR_TOOL_ROTATION_ID):
                {
                    if (_clockwise == FALSE)
                    {
                        HICON hIcon = OgwwUtils::CreateIconFromBytes(
                                          ICO_CLOCKWISE_16_Bytes(),
                                          ICO_COUNTERCLOCKWISE_16_ByteCount(), 16, 16);
                        OgwwStatic::SetIcon(_pweakACTOR_TOOL_ROTATION, hIcon, false);
                        _clockwise = TRUE;
                    }
                    else
                    {
                        HICON hIcon = OgwwUtils::CreateIconFromBytes(
                                          ICO_COUNTERCLOCKWISE_16_Bytes(),
                                          ICO_COUNTERCLOCKWISE_16_ByteCount(), 16, 16);
                        OgwwStatic::SetIcon(_pweakACTOR_TOOL_ROTATION, hIcon, false);
                        _clockwise = FALSE;
                    }
                    break; // Returns true and continues processing this message.
                }
                case ((WPARAM)ACTOR_TOOL_STATICTEST_ID):
                {
                    if (_ststictest == 0)
                    {
                        OgwwGenericWindow::RemoveStyleFlag(_pweakACTOR_TOOL_STATICTEST,
                                                           SS_CENTERIMAGE);
                        _ststictest++;
                        ::SendMessage(hWnd, WM_SIZE, 0, 0);
                   }
                    else
                    {
                        OgwwGenericWindow::AddStyleFlag(_pweakACTOR_TOOL_STATICTEST,
                                                        SS_CENTERIMAGE);
                        _ststictest = 0;
                        ::SendMessage(hWnd, WM_SIZE, 0, 0);
                    }
                }
                case ((WPARAM)ACTOR_TOOL_COLORTILE01_ID):
                {
                    break; // Returns true and continues processing this message.
                }
                case ((WPARAM)ACTOR_TOOL_COLORTILE02_ID):
                {
                    break; // Returns true and continues processing this message.
                }
           }
        }
    }

    return true;
}

Besides calls to my library, like OgwwGenericWindow::AddStyleFlag or OgwwGenericWindow::RemoveStyleFlag, you can also find direct calls to the Win32 API, like ::SendMessage.

This is the first approach to an expansion: Direct Win32 API calls.

A closer look at the AddOpenGlContent method reveals that Blank control has different constructors, such as OgwwBlank.ConstructPlane and OgwwBlank.ConstructCanvas, which prepare Blank control for different uses.

This is the second approach to an expansion: Add specialized constructors to existing controls.

The control implementations within my library are very simple and often incomplete. However, it is very easy to implement new controls when looking at the implementation of an existing control, such as OgwwStatic.

These are these final additional approaches to an expansion: Complete the existing controls within the library or add new controls to the library.

Points of Interest

As the sample application demonstrates - it is possible to create a serious looking OpenGL application (with menu bar, toolbar, status bar and actor box) for ReactOS based on plain old Win32 API. In addition, a small library has been created which can be used to write almost code-identical applications in C/C++ and C#.

This makes one want to dive even deeper into this topic.

History

  • 27th October, 2019: Initial article

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)