关于在ReactOS上使用C/C++和C#的OpenGL信息

开课吧小一2021-04-02 17:20

介绍

ReactOS是Windows操作系统的一个开源替代品。即使ReactOS的第一个版本可以追溯到1998年,但ReactOS仍然没有 "稳定 "的版本。可能,最重要的原因是缺乏关注。

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

关于在ReactOS上使用C/C++和C#的OpenGL信息

为什么是 "第二步"?

本文根据提示。

- 在ReactOS上用C/C++实现OpenGL介绍

- 介绍ReactOS上的无资源嵌入式图标和

- ReactOS上的C#介绍

这就是为什么我称它为 "第二步",以实现一个严肃的OpenGL应用程序。

动机是什么?

1.我想测试一下ReactOS的极限。所以我必须忍受ReactOS对我的限制。

2.C/C++。没有Visual Studio,但有非常好的Code::Blocks或者简单易用的Dev -C++。

3.库。没有MFC、ATL等,但有稳定的Win32 API。

4.C#: 2.0版以后没有微软.NET运行时,也根本没有微软的csc.exe,但MONO 4.0.3包括csc.exe -- -- 用它编译的汇编也将在.NET运行时运行。

5.汇编。没有Windows窗体或WPF,但MONO优秀的P/Invoke。

我觉得局限性不大,可以试一试。

1.Code::Blocks不会让我错过任何东西。它与MinGW结合使用也很好。

2.既然MFC、ATL等都是基于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, GLFW, GLEW, GLee, SDL, OpenTK, ...只有轻微的重叠。这些库主要关注平台的独立性和对OpenGL的优化访问。虽然创建应用程序窗口也是几乎所有这些库的一部分,但这些库主要是为了支持全屏应用程序或应用程序的主窗口内容完全作为OpenGL画布的应用程序。

我的库被设计成只使用应用程序主窗口的部分内容作为OpenGL画布。完全没有封装OpenGL API。我的库一方面非常简陋,另一方面又很容易扩展。控件的源代码很短,注释很好,概念很容易转移到新的控件上。

背景

在《ReactOS上的C/C++ OpenGL介绍》一文中,已经说明了OpenGL应用的最简单的锅炉板代码在上述条件下运行,那么问题来了,通往一个严肃的OpenGL应用的道路是怎样的。我的要求是

- 典型的桌面应用程序的外观和感觉,包括菜单栏、工具栏和状态栏。

- 良好的用户交互控制

- 一个开放的GL窗口

- 弹性大小调整行为

关于在ReactOS上使用C/C++和C#的OpenGL信息

关于在ReactOS上使用C/C++和C#的OpenGL信息

这一点可以通过OpenGL测试来证明。

有一个BLANK控件,作为一个演员栏,展示固定宽度/动态高度。该actor bar包含。

o两个OgwwStatic控件,显示文本,被标记为通知父体事件,并显示固定大小。第一个在三角形和六边形之间切换,第二个在OpenGL动画的红/绿/蓝和青/黄/红褐色之间切换着色。

o两个OgwwStatic控件,显示图标,被标记为通知父体事件和演示固定大小。第一个在OpenGL动画的顺时针和逆时针旋转之间切换。第二个在拉伸和居中图标之间切换--但这只适用于ReactOS。

o两个OgwwBlank控件,显示纯背景色并演示固定大小。第一个有一个凸起的边框,第二个有一个下沉的边框。

 有一个OgwwBlank控件,它显示OpenGL动画并演示动态调整大小。

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

这一点在两个弹性布局测试中都有体现。

- 有两个OgwwStatic控件,显示文本并演示动态调整大小。

- 有三个OgwwButton控件,演示固定大小。第一个按钮显示一个图像。第二个图像显示不同的字体样式。第三个按钮是默认按钮。

- 有一个OgwwEdit控件,用于演示动态调整大小。

什么叫 "弹性布局"/动态宽度、高度或调整大小?

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

关于在ReactOS上使用C/C++和C#的OpenGL信息

与.NET实现Windows Forms的弹性布局的方法不同--在Windows Forms中,弹性是通过对接和填充来实现的--我的方法使用行和单元格或列和单元格。这种解决方案更类似于布局管理器,由Java AWT引入,并被GTK、wxWidgets、WPF等年轻的工具箱采用。

我的方法是基于布局行、布局列和布局单元格,可以用控件填充。它不支持padding(还没有)。空格必须由空单元格来实现。由于空单元格不一定需要Windows资源,我认为这是一个合适的解决方案。

我的方法支持布局行和布局列的任意嵌套。唯一的缺点是,你必须在布局行和布局列之间选择主布局。

我的库一方面非常简陋,另一方面又很容易扩展。控件的源代码很短,注释很好,而且这个概念很容易转移到新的控件上。如果你想扩展这个概念,我推荐的文章有:《可调整对话框的自动布局》、《ClassLib,一个C++类库》、《Sizers: An Extendable Layout Management Library, Sharp Layout, or many more.

使用代码

公约

在ReactOS上,我使用Code::Blocks与MinGW开发环境。MinGW以函数名装饰而闻名,它与微软Visual Studio使用的函数名装饰有部分不兼容。Win32 API是用__stdcall调用惯例编译的,而MinGW的__stdcall函数名装饰与之不匹配。所以我决定在我的库中使用__cdecl调用约定。

Copy Code
                        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)。

C++
Copy Code
/// <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)。

C#
Copy Code
/// <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();

上面的例子,被选作示范,还有一个特殊的功能。返回值是一个 "Non-Blittable "数据类型--本例中的字符串。像字符串这样的 "Non-Blittable "数据类型总是由.NET自动释放。库API必须考虑到这一点,并提供以及接受这些数据类型作为副本(可以将内存管理交给.NET)。

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

申请样本

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

关于在ReactOS上使用C/C++和C#的OpenGL信息

应用程序初始化I

这就是创建MainFrame控件、MenuBar控件、ToolBar控件和StatusBar控件的C#代码的样子。

C#
Copy Code
/// <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。为了方便访问,这个类提供了Singleton这个字段。

模块句柄和以前的模块句柄是Win32 API所必需的。然而,我将避免研究以前的模块句柄。

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

C#
Copy Code
/// <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...)。关于独特/弱指针概念的描述,请参见智能指针。

C#
Shrink ▲   Copy Code
/// <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。这是初始化部分...

C#
Shrink ▲   Copy Code
/// <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通过调用AddMenueBar、AddStatusBar、AddToolBar和AddOpenGlContent来初始化MenuBar、StatusBar、ToolBar和主内容。

C#
Shrink ▲   Copy Code
/// <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条目,我使用了MENU_FILE_NEW_ID、MENU_FILE_OPEN_ID、MENU_FILE_EXIT_ID、MENU_TEST_CASE1_ID、MENU_TEST_CASE2_ID、MENU_TEST_CASE3_ID和MENU_HELP_ID等ID。

C#
Copy Code
/// <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!!!");
}

状态条使用了所有可能的最简单的变体--只有一个部分。

C#
Shrink ▲   Copy Code
/// <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条目,我重新使用了MENU_FILE_NEW_ID、MENU_FILE_OPEN_ID和MENU_HELP_ID等菜单条目ID。

这很简单,也不复杂。

应用初始化II

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

关于在ReactOS上使用C/C++和C#的OpenGL信息

这是OpenGL测试的构造代码。

C#
Shrink ▲   Copy Code
/// <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测试的销毁代码。

C#
Shrink ▲   Copy Code
/// <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;
}

改为布局测试

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

关于在ReactOS上使用C/C++和C#的OpenGL信息

这是行铺器测试的构造代码。

C#
Shrink ▲   Copy Code
/// <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有一个销毁代码。

C#
Shrink ▲   Copy Code
/// <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来处理事件(它的子事件)。事件处理程序的样子是这样的。

C#
Shrink ▲   Copy Code
/// <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::AddStyleFlag或OgwwGenericWindow::RemoveStyleFlag,你还可以找到对Win32 API的直接调用,比如::SendMessage。

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

仔细观察AddOpenGlContent方法,可以发现Blank控件有不同的构造函数,如OgwwBlank.ConstructPlane和OgwwBlank.ConstructCanvas,它们为Blank控件的不同用途做准备。

这是第二种扩展的方法。在现有的控件中加入专门的构造函数。

我的库内的控件实现非常简单,而且往往不完整。但是,在查看现有控件的实现时,实现新的控件是非常容易的,比如OgwwStatic。

这就是最后这些额外的扩展方法。完善库内现有的控件,或者在库中添加新的控件。

感悟

正如示例应用程序所展示的那样--基于普通的Win32 API,可以为ReactOS创建一个外观严肃的OpenGL应用程序(带有菜单栏、工具栏、状态栏和演员框)。此外,我们还创建了一个小型库,可用于在C/C++和C#中编写几乎相同代码的应用程序。

这让人不禁想更深入地研究这个话题。更多C/C++教程,尽在开课吧C/C++教程频道。

有用
分享