65.9K
CodeProject 正在变化。 阅读更多。
Home

在 ReactOS(或 Windows)上使用 C/C++ 和 C# 进行 OpenGL 的更多信息

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2019年10月27日

CPOL

11分钟阅读

viewsIcon

10021

downloadIcon

436

使用纯粹的 Win32 API 在 ReactOS( 以及 Windows 或 WINE) 上实现严肃外观的 OpenGL 应用程序的第二步

引言

ReactOSWindows 操作系统的一个开源替代方案。尽管 ReactOS 的第一个版本可以追溯到 1998 年,但仍然没有一个“稳定”版本的 ReactOS。也许最重要的原因是缺乏关注。

本文的所有结果也可在 Windows 上运行(已在 Windows 10 - 64 位版本上测试)。

为什么是“第二”步?

本文基于以下提示

这就是为什么我称之为“第二步”,迈向一个看起来严肃的 OpenGL 应用程序。

动机是什么?

我想测试 ReactOS 的极限。因此,我必须忍受 ReactOS 施加给我的限制

  1. C/C++:没有 Visual Studio,但有非常好的 Code::Blocks 或易于使用的 Dev-C++
  2. 库:没有 MFCATL 等,但有稳定的 Win32 API
  3. C#:没有 Microsoft .NET 运行时版本 2.0 以上,也没有 Microsoft csc.exe,但有 MONO 4.0.3 包括 csc.exe - 使用它编译的程序集也将在 .NET 运行时上运行
  4. 程序集:没有 Windows FormsWPF,但有 MONO 优秀的 P/Invoke

我不认为这些限制太严苛而不能尝试

  1. Code::Blocks 让我没有什么可缺少的。它与 MinGW 结合使用效果很好。
  2. 由于 MFCATL 等都基于 Win32 API - 在没有 MFC、ATL 等的情况下工作有什么限制?我的回答是:我只会错过更短的代码和动态布局(控件)。这两种都可以很容易地用 Win32 API 的薄封装来实现。
  3. MONO 的 csc.exe 只让我错过一件事:资源。但图像和图标的嵌入式二进制表示可以很容易地弥补这一差距。
  4. 对于 Windows Forms,我会说,这与 MFC 相同:更短的代码和动态布局(控件)可以很容易地用 Win32 API 的薄封装来实现。

目标是什么?

在最好的情况下:一个库,它能显著缩短 C/C++ 代码,提供控件的动态布局,并且也可以从 C# 使用。我同时考虑 C/C++ 和 C#,因为我还没有决定:C/C++(快速且 100% 受我控制)还是 C#(舒适而优雅)。

我的库是“又一个 OpenGL 工具”吗?

我不这么认为,因为我的库与 GLUT/freeglut、GLFWGLEWGLeeSDLOpenTK 等只有微小的重叠。这些库主要侧重于平台独立性和对 OpenGL 的优化访问。虽然创建应用程序窗口也是几乎所有这些库的一部分,但这些库主要设计用于支持全屏应用程序或应用程序中应用程序主窗口的内容完全用作 OpenGL 画布的情况。

我的库设计用于仅使用应用程序主窗口的一部分内容作为 OpenGL 画布。OpenGL API 完全没有被封装。我的库一方面非常基础,另一方面易于扩展。控件的源代码简短、注释良好,并且概念易于移植到新控件。

背景

在文章 在 ReactOS 上使用 C/C++ 进行 OpenGL 入门 已经证明,最简单的 OpenGL 应用程序样板代码可以在上述条件下运行之后,问题就来了,一个严肃的 OpenGL 应用程序的路径可能是什么样的?我的要求是

  • 桌面应用程序的典型外观和感觉,包括菜单栏、工具栏和状态栏
  • 用于用户交互的体面控件
  • 一个 Open-GL 窗口
  • 弹性调整大小行为

在 ReactOS 上看起来可能像这样

而在 Windows 10 上看起来像这样

我创建了一个示例应用程序,其中包含三个测试用例,以验证是否符合我的要求
  • 一个 OpenGL 测试,仅使用应用程序主窗口的部分内容。
  • 一个用于弹性调整大小行为的测试,使用行布局。
  • 以及一个用于弹性调整大小行为的测试,使用列布局。

示例应用程序的测试菜单可用于在这三个测试之间动态切换。已完成测试用例中所有不再需要的控件都将被动态移除,而下一个测试用例所需的所有新控件都将被动态创建。

弹性行布局,带有默认样式的按钮

弹性列布局,带有扁平样式的按钮

这通过 OpenGL 测试得以证明

  • 有一个 BLANK 控件,用作执行器条,并演示固定宽度/动态高度。执行器条包含
    • 两个 OgwwStatic 控件,显示文本,并标记为通知父窗口有关事件,演示固定大小。第一个在三角形和六边形之间切换,第二个在红/绿/蓝和青色/黄色/品红色 OpenGL 动画的着色之间切换。
    • 两个 OgwwStatic 控件,显示图标,并标记为通知父窗口有关事件,演示固定大小。第一个在 OpenGL 动画的顺时针和逆时针旋转之间切换。第二个在拉伸和居中图标之间切换 - 但这仅适用于 ReactOS。
    • 两个 OgwwBlank 控件,显示纯色背景,演示固定大小。第一个有一个凸起的边框,第二个有一个凹陷的边框。
  • 有一个 OgwwBlank 控件,显示 OpenGL 动画并演示动态调整大小。

注意:显示 OpenGL 上下文的控件必须是最后创建的。否则,OpenGL 视口计算将不适合控件大小。

这通过两个弹性布局测试得以证明

  • 有两个 OgwwStatic 控件,显示文本并演示动态调整大小。
  • 有三个 OgwwButton 控件,演示固定大小。第一个按钮显示图像。第二个按钮显示不同的字体样式。第三个按钮是默认按钮。
  • 有一个 OgwwEdit 控件,演示动态调整大小。

我说的“弹性布局”/动态宽度、高度或调整大小是什么意思?

弹性布局允许控件之间进行相对定位,并在调整父窗口大小时保持控件的相对位置。控件可以根据父窗口的大小进行适当调整/成比例调整(这种行为应用于我示例应用程序中的静态文本控件和编辑控件的弹性布局测试),或保留其原始大小并适应中间空间(这种行为应用于我示例应用程序中的所有三个按钮控件的弹性布局测试)。

与 .NET 实现 Windows Forms 弹性布局的方法不同(在该方法中,通过停靠和填充来实现弹性)- 我的方法使用行和单元格或列和单元格。此解决方案更类似于 布局管理器,它由 Java AWT 引入,并被 GTKwxWidgetsWPF 等较新的工具包采用。

我的方法基于行布局、列布局和可填充控件的布局单元格。它(目前)不支持填充。空间必须通过空单元格来实现。由于空单元格不一定需要 Windows 资源,我认为这是一个合适的解决方案。
我的方法支持行布局和列布局的任意嵌套。唯一的缺点是您必须选择行布局和列布局之间的主要布局。

我的库一方面非常基础,另一方面易于扩展。控件的源代码简短、注释良好,并且概念易于移植到新控件。 如果您想扩展此概念,我建议阅读文章 自动布局可调整大小的对话框ClassLib,一个 C++ 类库Sizers:一个可扩展的布局管理库Sharp Layout,或者更多。

Using the Code

约定

在 ReactOS 上,我使用 Code::BlocksMinGW 开发环境。MinGW 以其部分不兼容 Microsoft Visual Studio 所使用的函数名修饰而出名。Win32 API 使用 __stdcall 调用约定进行编译,而 MinGW__stdcall 的函数名修饰与之不匹配。因此,我决定为我的库使用 __cdecl 调用约定。

                        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

因此,C/C++ 中导入函数的声明如下所示(并使用 __cdecl

/// <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();

C# 中导入函数的声明如下所示(并使用 CallingConvention = CallingConvention.Cdecl

/// <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();

上面选择用于演示的示例具有另一个特殊功能:返回值是“非托管”数据类型 - 在此情况下是一个字符串。“非托管”数据类型(如 string)始终由 .NET 自动释放。Library API 必须考虑到这一点,并提供和接受这些作为副本的数据类型(可以将其内存管理交给 .NET)。

我的库仅使用 WCHAR(Unicode 字符)- 以提供与 .NET 的最大兼容性。只有一个例外 - CreateWindowW 的 ReactOS 实现不能正确处理 windowName 参数,所以我改用 CreateWindowA(因此也使用 RegisterClassA)。

示例应用程序

我的 MainFrame 控件代表应用程序主窗口。MenuBar 控件、ToolBar 控件和 StatusBar 控件由 MainFrame 控件自动管理 - 这意味着可调整大小的布局由应用程序窗口管理。我的 MainFrame 控件可以处理剩余空间中的任何控件或任何布局器。我的 OpenGL 测试、行布局器测试和列布局器测试分别向应用程序窗口的剩余空间注册一个布局器 - 每个测试都提供自己的布局器。

应用程序初始化 I

这是 C# 代码,用于创建 MainFrame 控件、MenuBar 控件、ToolBar 控件和 StatusBar 控件

/// <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();
    }
}

我的 TheApplication 类是单例。为了方便访问,此类提供了 Singleton 字段。

模块句柄和前一个模块句柄是 Win32 API 所必需的。但是,我将不研究前一个模块句柄。

第 0010 行和第 0011 行构造并运行应用程序。背后的代码如下...

/// <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;
}

由于我的 Win32 API 包装器库不原生处理 MainFrame 控件指针,因此应用程序必须将其视为唯一指针(_puniqueMainFrame)。所有其他控件都由 Win32 API 包装器库原生处理,并由应用程序视为弱指针(_pweakStatusBar...)。有关唯一/弱指针概念的描述,请参阅 智能指针

/// <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();
}

由于我的库是基于 Win32 的,应用程序必须提供一个 WindowProc 来处理事件。我通过返回值实现这一点,以便应用程序的 WindowsProc 可以决定是继续处理消息(true)还是不处理(false)。

由于应用程序的 WindowsProc 在库的 WindowsProc 之前被调用,我称之为 *PreprocessCallback。这是初始化部分...

/// <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;
}

应用程序的 WindowsProc 通过调用 AddMenueBarAddStatusBarAddToolBarAddOpenGlContent 来初始化 MenuBarStatusBarToolbar 和主内容。

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

为了能够将应用程序 WindowsProc 中的消息分配给 MenuBar 条目,我使用了 IDs MENU_FILE_NEW_IDMENU_FILE_OPEN_IDMENU_FILE_EXIT_IDMENU_TEST_CASE1_IDMENU_TEST_CASE2_IDMENU_TEST_CASE3_IDMENU_HELP_ID

/// <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!!!");
}

StatusBar 使用所有可能变体中最简单的一种 - 只有 1 部分。

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

为了能够将应用程序 WindowsProc 中的消息分配给 ToolBar 条目,我重新使用了菜单项 ID MENU_FILE_NEW_IDMENU_FILE_OPEN_IDMENU_HELP_ID

这非常简单明了。

应用程序初始化 II

下一步是填充应用程序窗口的主内容。我将从 OpenGL 测试开始 - 它应该支持弹性布局。这是我的详细设计方法

这是 OpenGL 测试的构造代码

/// <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;
}

与基于资源的 GUI 创建相比,以编程方式创建 GUI 的最大好处在于能够完全或部分动态地构建和解构 GUI。

这是 OpenGL 测试的销毁代码

/// <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;
}

切换到布局测试

由于行布局器和列布局器的弹性布局测试非常相似,我将只在这里展示行布局器。这是我的详细设计方法

这是行布局器测试的构造代码

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

行布局器测试与 OpenGL 测试一样 - 有一个 GUI 的销毁代码

/// <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;
}

库扩展

我想回到 AddOpenGlContent 方法。由于它使用了两个行布局器,因此布局器通过一个 Plane 控件互联。Plane 控件还必须有一个 WindowProc 来处理事件(它是子事件)。事件处理程序如下所示

/// <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;
}

除了调用我的库(如 OgwwGenericWindow::AddStyleFlagOgwwGenericWindow::RemoveStyleFlag)之外,您还可以找到直接调用 Win32 API 的地方,例如 ::SendMessage

这是扩展的第一种方法:直接 Win32 API 调用。

仔细查看 AddOpenGlContent 方法会发现,Blank 控件有不同的构造函数,例如 OgwwBlank.ConstructPlaneOgwwBlank.ConstructCanvas,它们为 Blank 控件准备了不同的用途。

这是扩展的第二种方法:为现有控件添加专用构造函数。

我的库中的控件实现非常简单,而且常常不完整。但是,查看现有控件(如 OgwwStatic)的实现,实现新控件非常容易。

这些是扩展的最终附加方法:完成库中现有控件或向库添加新控件。

关注点

正如示例应用程序所示 - 有可能基于纯旧的 Win32 API 为 ReactOS 创建一个看起来严肃的 OpenGL 应用程序(带有菜单栏、工具栏、状态栏和执行器框)。此外,还有一个小型库,可用于在 C/C++ 和 C# 中编写几乎代码相同的应用程序。

这让人想更深入地研究这个主题。

历史

  • 2019 年 10 月 27 日:初稿
© . All rights reserved.