使用 WinUI 3 和 Win32 互操作生成 C# .NET 应用

在本文中,我们逐步演练如何使用平台调用服务 (PInvoke) 生成具有 WinUI 3 和 Win32 互操作功能的基本 C# .NET 应用程序。

先决条件

  1. 设置开发环境,如安装适用于 Windows 应用 SDK 的工具所述。
  2. 按照创建第一个 WinUI 3 项目中的步骤测试配置。

基本托管 C#/.NET 应用

对于此示例,我们将指定应用窗口的位置和大小,根据相应的 DPI 进行转换和缩放,禁用窗口最小化和最大化按钮,最后查询当前进程以显示当前进程中加载的模块列表。

我们将从初始模板应用程序构建我们的示例应用(请参阅先决条件)。 另请参阅 Visual Studio 中的 WinUI 3 模板

MainWindow.xaml 文件

凭借 WinUI 3,你可以通过 XAML 标记创建 Window 类的实例。

XAML Window 类已扩展为支持桌面窗口,从而转换为 UWP 和桌面应用模型所使用的每个低级别窗口实现的抽象。 具体而言是适用于 UWP 的 CoreWindow 以及适用于 Win32 的窗口句柄(或 HWND)。

以下代码演示初始模板应用的 MainWindow.xaml 文件,该文件使用 Window 类作为应用的根元素。

<Window
    x:Class="WinUI_3_basic_win32_interop.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI_3_basic_win32_interop"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
        <Button x:Name="myButton" Click="myButton_Click">Click Me</Button>
    </StackPanel>
</Window>

Configuration

  1. 若要从 User32.dll 调用 Win32 API,请将开放源代码 PInvoke.User32 NuGet 包添加到 VS 项目中(从 Visual Studio 菜单中,选择“工具”->“NuGet 包管理器”->“管理解决方案的 NuGet 包…”,然后搜索“Pinvoke.User32”)。 有关更多详细信息,请参阅从托管代码调用本机函数

    Screenshot of the Visual Studio NuGet Package Manager with PInvoke.User32 selected.
    选择了 PInvoke.User32 的 NuGet 包管理器。

    通过检查 VS 项目中的 Packages 文件夹来确认安装已成功。

    Screenshot of the Visual Studio Solution Explorer Packages with PInvoke.User32.
    包含 PInvoke.User32 的解决方案资源管理器包。

    接下来,双击应用程序项目文件(或右键单击并选择“编辑项目文件”)以在文本编辑器中打开该文件,然后确认项目文件现在包含“PInvoke.User32”的 NuGet PackageReference

    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <OutputType>WinExe</OutputType>
        <TargetFramework>net6.0-windows10.0.19041.0</TargetFramework>
        <TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
        <RootNamespace>WinUI_3_basic_win32_interop</RootNamespace>
        <ApplicationManifest>app.manifest</ApplicationManifest>
        <Platforms>x86;x64;arm64</Platforms>
        <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers>
        <UseWinUI>true</UseWinUI>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="Microsoft.ProjectReunion" Version="0.8.1" />
        <PackageReference Include="Microsoft.ProjectReunion.Foundation" Version="0.8.1" />
        <PackageReference Include="Microsoft.ProjectReunion.WinUI" Version="0.8.1" />
        <PackageReference Include="PInvoke.User32" Version="0.7.104" />
        <Manifest Include="$(ApplicationManifest)" />
      </ItemGroup>
    </Project>
    

代码

  1. App.xaml.cs 代码隐藏文件中,我们使用 WindowNative.GetWindowHandle WinRT COM 互操作方法获取窗口的句柄(请参阅检索窗口句柄 (HWND))。

    此方法从应用的 OnLaunched 处理程序进行调用,如下所示:

    /// <summary>
    /// Invoked when the application is launched normally by the end user.  Other entry points
    /// will be used such as when the application is launched to open a specific file.
    /// </summary>
    /// <param name="args">Details about the launch request and process.</param>
    protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
    {
        m_window = new MainWindow();
    
        var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(m_window);
    
        SetWindowDetails(hwnd, 800, 600);
    
        m_window.Activate();
    }
    
  2. 我们随后调用 SetWindowDetails 方法,从而传递窗口句柄和首选尺寸。 请记得添加 using static PInvoke.User32; 指令。

    在此方法中:

    • 我们调用 GetDpiForWindow 以获取窗口的每英寸点数 (dpi) 值(Win32 使用实际像素,而 WinUI 3 使用有效像素)。 此 dpi 值用于计算缩放因子,并将它应用于为窗口指定的宽度和高度。
    • 我们随后调用 SetWindowPos 以指定窗口的所需位置。
    • 最后,我们调用 SetWindowLong 以禁用“最小化”和“最大化”按钮 。
    private static void SetWindowDetails(IntPtr hwnd, int width, int height)
    {
        var dpi = GetDpiForWindow(hwnd);
        float scalingFactor = (float)dpi / 96;
        width = (int)(width * scalingFactor);
        height = (int)(height * scalingFactor);
    
        _ = SetWindowPos(hwnd, SpecialWindowHandles.HWND_TOP,
                                    0, 0, width, height,
                                    SetWindowPosFlags.SWP_NOMOVE);
        _ = SetWindowLong(hwnd, 
               WindowLongIndexFlags.GWL_STYLE,
               (SetWindowLongFlags)(GetWindowLong(hwnd,
                  WindowLongIndexFlags.GWL_STYLE) &
                  ~(int)SetWindowLongFlags.WS_MINIMIZEBOX &
                  ~(int)SetWindowLongFlags.WS_MAXIMIZEBOX));
    }
    
  3. 在 MainWindow.xaml 文件中,我们将 ContentDialogScrollViewer 一起使用,来显示为当前进程加载的所有模块的列表。

    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
        <Button x:Name="myButton" Click="myButton_Click">Display loaded modules</Button>
    
        <ContentDialog x:Name="contentDialog" CloseButtonText="Close">
            <ScrollViewer>
                <TextBlock x:Name="cdTextBlock" TextWrapping="Wrap" />
            </ScrollViewer>
        </ContentDialog>
    
    </StackPanel>
    
  4. 然后,将 MyButton_Click 事件处理程序替换为以下代码。

    在这里,我们通过调用 GetCurrentProcess 来获取对当前进程的引用。 我们随后循环访问模块的集合并将每个 ProcessModule 的文件名追加到显示字符串。

    private async void myButton_Click(object sender, RoutedEventArgs e)
    {
        myButton.Content = "Clicked";
    
        var description = new System.Text.StringBuilder();
        var process = System.Diagnostics.Process.GetCurrentProcess();
        foreach (System.Diagnostics.ProcessModule module in process.Modules)
        {
            description.AppendLine(module.FileName);
        }
    
        cdTextBlock.Text = description.ToString();
        await contentDialog.ShowAsync();
    }
    
  5. 编译并运行应用。

  6. 窗口出现后,选择“显示加载的模块”按钮。

    Screenshot of the basic Win32 interop application described in this topic.
    本主题中所述的基本 Win32 互操作应用程序。

总结

在本主题中,我们介绍了如何访问基础窗口实现(本例中为 Win32 和 HWND),以及使用 Win32 API 和 WinRT API。 这演示了如何在创建新 WinUI 3 桌面应用时使用现有桌面应用程序代码。

有关更广泛的示例,请参阅 Windows 应用 SDK 示例 GitHub 存储库中的 AppWindow 库示例

请参阅