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:
- C/C++: No Visual Studio, but the very good Code::Blocks or the easy to use Dev-C++
- Library: No MFC, ATL and so on, but a stable Win32 API
- 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
- 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:
- Code::Blocks doesn't make me miss anything. It works fine in combination with MinGW.
- 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.
- MONO's csc.exe only makes me miss one thing: Resources. But embedded binary representation of images and icons can easily bridge this gap.
- 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:
| And on Windows 10 like this:
|
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.
|
|
Elastic row layout, buttons with default style
|
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).
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
):
extern LPCWSTR __cdecl Utils_CoGetDebugLevelName();
The declarations of imported functions in C# look like this (and use CallingConvention = CallingConvention.Cdecl
):
[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 string
s 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.
Application Initialization I
This is how the C# code looks like to create MainFrame control, MenuBar control, ToolBar control and StatusBar control:
public static void Main(string[] args)
{
try
{
HINSTANCE hModule = ::GetModuleHandle(null);
TheApplication.Singleton = new TheApplication(hModule, IntPtr.Zero);
TheApplication.Singleton.Run(args);
TheApplication.Singleton.Dispose();
System.Threading.Thread.Sleep(3000);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
System.Threading.Thread.Sleep(3000);
}
}
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...
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.
void TheApplication::Run(string[] args)
{
Win32.InitCommonControls();
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...
public BOOL MainWindowMessageLoopPreprocessCallback(HWND hWnd, UINT msg,
IntPtr wParam, IntPtr lParam)
{
switch (msg)
{
case WM.CREATE:
{
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
.
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
.
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.
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,
(BYTE)4,
(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,
(BYTE)4,
(BYTE)0);
OgwwToolBar.AddButton(_pweakToolBar,
IntPtr.Zero,
IntPtr.Zero,
(UINT)0,
(BYTE)0,
(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,
(BYTE)4,
(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:
This is the construction code for the OpenGL test:
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;
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);
OgwwBlank.RegisterMessageLoopPreprocessCallback(pweakActorPlane,
new OgwwGenericWindow.WNDPROCCB(this.ActorPlaneMessageLoopPreprocessCallback));
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, 1);
OgwwBlank.SetFrameFlags(pweakPlane01, 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, 2);
OgwwBlank.SetFrameFlags(pweakPlane02, 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);
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:
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;
HROWLAYOUTER puniqueRow = OgwwRowLayouter::RemoveLast(_pweakOglLayouter);
while (puniqueRow != NULL)
{
HLAYOUTERCELL puniqueCell = OgwwLayouterRow::RemoveLast(puniqueRow);
while (puniqueCell != NULL)
{
if (OgwwLayouterCell::Window(puniqueCell) == _pweakOglCanvas)
{
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)
{
HROWLAYOUTER puniqueSubRow =
OgwwRowLayouter::RemoveLast(puniqueSubLayouter);
while (puniqueSubRow != NULL)
{
HLAYOUTERCELL puniqueSubCell =
OgwwLayouterRow::RemoveLast(puniqueSubRow);
while (puniqueSubCell != NULL)
{
OgwwLayouterCell::Destruct(puniqueSubCell, true);
puniqueSubCell = NULL;
puniqueSubCell = OgwwLayouterRow::RemoveLast(puniqueSubRow);
}
OgwwLayouterRow::Destruct(puniqueSubRow);
puniqueSubRow = NULL;
puniqueSubRow = OgwwRowLayouter::RemoveLast(puniqueSubLayouter);
}
}
}
}
OgwwLayouterCell::Destruct(puniqueCell, true);
puniqueCell = NULL;
puniqueCell = OgwwLayouterRow::RemoveLast(puniqueRow);
}
OgwwLayouterRow::Destruct(puniqueRow);
puniqueRow = NULL;
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:
This is the construction code for the row layouter test:
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);
::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:
void TheApplication::RemoveRowLayoutContent(HWND hWnd)
{
if (_pweakRowLayouter == NULL)
return;
HROWLAYOUTER puniqueRow = OgwwRowLayouter::RemoveLast(_pweakRowLayouter);
while (puniqueRow != NULL)
{
HLAYOUTERCELL puniqueCell = OgwwLayouterRow::RemoveLast(puniqueRow);
while (puniqueCell != NULL)
{
OgwwLayouterCell::Destruct(puniqueCell, true);
puniqueCell = NULL;
puniqueCell = OgwwLayouterRow::RemoveLast(puniqueRow);
}
OgwwLayouterRow::Destruct(puniqueRow);
puniqueRow = NULL;
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:
bool ActorPlaneMessageLoopPreprocessCallback(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
switch(msg)
{
case WM_COMMAND:
{
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;
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;
}
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;
}
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;
}
case ((WPARAM)ACTOR_TOOL_COLORTILE02_ID):
{
break;
}
}
}
}
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