将 winapp CLI 与 Flutter 配合使用

有关完整的工作示例,请查看此存储库中的 Flutter 示例 。

本指南演示如何将 winapp CLI 与 Flutter 应用程序配合使用,以 MSIX 的形式添加包标识和打包应用。

包标识是Windows app模型中的核心概念。 它允许应用程序访问特定的Windows API(例如通知、安全、AI API 等),具有干净的安装/卸载体验等。

标准 Flutter Windows 生成没有包标识。 本指南演示如何添加它进行调试,然后将其打包以供分发。

先决条件

  1. Flutter SDK:按照 官方指南安装 Flutter。

  2. winapp CLI:通过 winget 安装或更新winapp CLI(如果已安装)。

    winget install Microsoft.winappcli --source winget
    

1. 创建新的 Flutter 应用

按照官方 Flutter 文档中的指南创建新的应用程序并运行它。

您应该能看到默认的 Flutter 计数器应用。

2. 更新代码以检查标识

我们将更新应用,以检查它是否使用包标识运行。 我们将使用 Dart FFI 调用 Windows GetCurrentPackageFamilyName API。

首先,添加 ffi 包:

flutter pub add ffi

接下来,用以下代码替换 lib/main.dart 的内容。 此代码尝试使用 Windows API 检索当前包标识。 如果成功,则会在 UI 中显示包系列名称;否则,它会显示“未打包”。

import 'dart:ffi';
import 'dart:io' show Platform;

import 'package:ffi/ffi.dart';
import 'package:flutter/material.dart';

/// Returns the Package Family Name if running with package identity, or null.
String? getPackageFamilyName() {
  if (!Platform.isWindows) return null;

  final kernel32 = DynamicLibrary.open('kernel32.dll');
  final getCurrentPackageFamilyName = kernel32.lookupFunction<
      Int32 Function(Pointer<Uint32>, Pointer<Uint16>),
      int Function(
          Pointer<Uint32>, Pointer<Uint16>)>('GetCurrentPackageFamilyName');

  final length = calloc<Uint32>();
  try {
    // First call to get required buffer length
    final result =
        getCurrentPackageFamilyName(length, Pointer<Uint16>.fromAddress(0));
    if (result != 122) return null; // ERROR_INSUFFICIENT_BUFFER = 122

    // Second call with buffer to get the name
    final namePtr = calloc<Uint16>(length.value);
    try {
      final result2 = getCurrentPackageFamilyName(length, namePtr);
      if (result2 == 0) {
        return namePtr.cast<Utf16>().toDartString(); // ERROR_SUCCESS = 0
      }
      return null;
    } finally {
      calloc.free(namePtr);
    }
  } finally {
    calloc.free(length);
  }
}

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  late final String? _packageFamilyName;

  @override
  void initState() {
    super.initState();
    _packageFamilyName = getPackageFamilyName();
  }

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Container(
              padding: const EdgeInsets.all(16),
              margin: const EdgeInsets.only(bottom: 24),
              decoration: BoxDecoration(
                color: _packageFamilyName != null
                    ? Colors.green.shade50
                    : Colors.orange.shade50,
                borderRadius: BorderRadius.circular(8),
                border: Border.all(
                  color: _packageFamilyName != null
                      ? Colors.green
                      : Colors.orange,
                ),
              ),
              child: Text(
                _packageFamilyName != null
                    ? 'Package Family Name:\n$_packageFamilyName'
                    : 'Not packaged',
                textAlign: TextAlign.center,
                style: Theme.of(context).textTheme.bodyLarge,
              ),
            ),
            const Text('You have pushed the button this many times:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

3.在没有标识的情况下运行

现在,像往常一样生成并运行应用:

flutter build windows

直接运行可执行文件(如果项目名称不同,请替换为 flutter_app):

.\build\windows\x64\runner\Release\flutter_app.exe

小窍门

无论计算机的体系结构如何,生成输出都位于 x64 文件夹中,这应该适用于 Flutter 的Windows生成。

你应该会看到带有橙色“未打包”指示器的应用程序。 这确认标准可执行文件在没有任何包标识的情况下正在运行。

4.使用 winapp CLI 初始化Project

winapp init 命令一次性设置好所需的一切:应用清单、资产和可用于 C++ 开发的(可选)Windows 应用 SDK 标头。 清单定义应用的标识(名称、发布者、版本),Windows用于授予 API 访问权限。

运行以下命令并按照提示操作:

winapp init

出现提示时:

  • 包名称:按 Enter 接受默认值(派生自项目名称)
  • 发布者名称:按 Enter 接受默认值或输入名称
  • 版本:按 Enter 接受 1.0.0.0
  • Description:按 Enter 接受默认值(Windows应用程序)
  • 设置 SDK:选择 "Stable SDKs" 以下载 Windows 应用 SDK 并生成 C++ 头文件(步骤 6 需要)

此命令将:

  • 创建 Package.appxmanifest — 定义您应用身份的清单
  • 创建 Assets 文件夹 - MSIX 打包和应用商店提交所需的图标
  • 使用Windows 应用 SDK标头和库创建 .winapp 文件夹
  • 创建一个 winapp.yaml 配置文件用于锁定 SDK 版本

可以打开 Package.appxmanifest 以进一步自定义属性,如显示名称、发布者和功能。

使用身份进行调试

若要测试需要标识(如通知)且未完全打包应用的功能,可以使用 winapp run。 这会注册松散布局包(就像真正的 MSIX 安装一样),并在一个步骤中启动应用。 调试不需要证书或签名。

  1. 生成应用:

    flutter build windows
    
  2. 使用标识运行:

    winapp run .\build\windows\x64\runner\Release
    

小窍门

winapp run 还会在您的系统中注册该软件包。 这就是为什么在步骤 7 的后面尝试安装 MSIX 时,MSIX 可能显示为“已安装”。 完成后使用 winapp unregister 清理开发包。

你现在应该看到带有绿色指示器的应用,显示:

Package Family Name: flutterapp.debug_xxxxxxxx

这确认你的应用正在运行,并具有有效的包标识!

小窍门

有关高级调试工作流(附加调试器、IDE 设置、启动调试),请参阅 调试指南。

6. 使用 Windows 应用 SDK (可选)

如果选择在 winapp init 期间设置 SDK,则现在可以访问 .winapp/include 文件夹中的 Windows 应用 SDK C++ 标头。 由于 Flutter 的Windows运行程序是 C++,因此可以从本机代码调用 Windows 应用 SDK API,并通过方法通道将其公开给 Dart。 如果你仅需要用于分发的包标识,可以跳到步骤 7。

让我们添加一个显示Windows 应用运行时版本的简单示例。

创建本机插件

创建 windows/runner/winapp_sdk_plugin.h:

#ifndef RUNNER_WINAPP_SDK_PLUGIN_H_
#define RUNNER_WINAPP_SDK_PLUGIN_H_

#include <flutter/flutter_engine.h>

// Registers a method channel for querying Windows App SDK info.
void RegisterWinAppSdkPlugin(flutter::FlutterEngine* engine);

#endif  // RUNNER_WINAPP_SDK_PLUGIN_H_

创建 windows/runner/winapp_sdk_plugin.cpp:

#include "winapp_sdk_plugin.h"

#include <flutter/method_channel.h>
#include <flutter/standard_method_codec.h>
#include <winrt/Microsoft.Windows.ApplicationModel.WindowsAppRuntime.h>

#include <string>

void RegisterWinAppSdkPlugin(flutter::FlutterEngine* engine) {
  auto channel = std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
      engine->messenger(), "com.example/winapp_sdk",
      &flutter::StandardMethodCodec::GetInstance());

  channel->SetMethodCallHandler(
      [](const flutter::MethodCall<flutter::EncodableValue>& call,
         std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
        if (call.method_name() == "getRuntimeVersion") {
          try {
            // Flutter already initializes COM in main.cpp, so we skip
            // winrt::init_apartment() here — the apartment is already set up.
            auto version = winrt::Microsoft::Windows::ApplicationModel::
                WindowsAppRuntime::RuntimeInfo::AsString();
            std::string versionStr = winrt::to_string(version);
            result->Success(flutter::EncodableValue(versionStr));
          } catch (const winrt::hresult_error& e) {
            result->Error("WINRT_ERROR", winrt::to_string(e.message()));
          } catch (...) {
            result->Error("UNKNOWN_ERROR",
                          "Failed to get Windows App Runtime version");
          }
        } else {
          result->NotImplemented();
        }
      });

  // prevent channel destruction by releasing ownership
  channel.release();
}

更新 CMakeLists.txt

编辑 windows/runner/CMakeLists.txt 以进行三项更改。 查找add_executable块并将"winapp_sdk_plugin.cpp"添加到源文件列表中。

add_executable(${BINARY_NAME} WIN32
  "flutter_window.cpp"
  "main.cpp"
  "utils.cpp"
  "win32_window.cpp"
  "winapp_sdk_plugin.cpp"       # <-- add this line
  "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
  "Runner.rc"
  "runner.exe.manifest"
)

然后在文件末尾添加这两行以链接 WinRT 库,并包括Windows 应用 SDK标头:

# Link Windows Runtime libraries for WinRT
target_link_libraries(${BINARY_NAME} PRIVATE "WindowsApp.lib")

# Windows App SDK headers from winapp CLI
target_include_directories(${BINARY_NAME} PRIVATE
  "${CMAKE_SOURCE_DIR}/../.winapp/include")

注册插件

在 windows/runner/flutter_window.cpp 文件中,将 include 语句添加到顶部,与文件中的其他 include 语句一起。

#include "winapp_sdk_plugin.h"

然后在 FlutterWindow::OnCreate() 中找到 RegisterPlugins 调用,并在其后的行上添加 RegisterWinAppSdkPlugin:

  RegisterPlugins(flutter_controller_->engine());
  RegisterWinAppSdkPlugin(flutter_controller_->engine());  // <-- add this line

更新 main.dart

在 lib/main.dart 顶部添加以下导入,与现有导入一起。

import 'package:flutter/services.dart';

请在现有 getPackageFamilyName() 函数的下方(在任何类之外)添加此函数:

/// Queries the Windows App Runtime version via a native method channel.
Future<String?> getWindowsAppRuntimeVersion() async {
  if (!Platform.isWindows) return null;
  try {
    const channel = MethodChannel('com.example/winapp_sdk');
    final version = await channel.invokeMethod<String>('getRuntimeVersion');
    return version;
  } catch (_) {
    return null;
  }
}

在类中 _MyHomePageState ,在现有 _packageFamilyName字段旁边添加新字段:

  late final String? _packageFamilyName;
  String? _runtimeVersion;         // <-- add this line

更新 initState() 以调用新函数:

  @override
  void initState() {
    super.initState();
    _packageFamilyName = getPackageFamilyName();
    // Fetch the runtime version asynchronously
    getWindowsAppRuntimeVersion().then((version) {
      setState(() {
        _runtimeVersion = version;
      });
    });
  }

最后,在方法中 build 显示运行时版本。 在 Column 子项列表中添加此小组件,就在显示包标识的 Container 之后:

            if (_runtimeVersion != null)
              Padding(
                padding: const EdgeInsets.only(bottom: 16),
                child: Text(
                  'Windows App Runtime: $_runtimeVersion',
                  style: Theme.of(context).textTheme.bodyLarge,
                ),
              ),

构建和运行

重新生成应用程序:

flutter build windows
winapp run .\build\windows\x64\runner\Release

现在应会看到如下所示的输出:

Package Family Name: flutterapp.debug_xxxxxxxx
Windows App Runtime: 8000.731.1532.0

.winapp/include 目录包含Windows 应用 SDK所需的所有标头,包括:

  • winrt/ - 用于访问Windows 运行时 API 的 WinRT C++ 投影标头
  • Microsoft.UI.*.h - 用于现代 UI 组件的 WinUI 3 头文件
  • MddBootstrap.h - Windows 应用 SDK引导过程
  • WindowsAppSDK-VersionInfo.h - 版本信息
  • 以及更多的 Windows 应用 SDK 组件

有关更高级Windows 应用 SDK用法,请查看 Windows 应用 SDK 文档。

7. 使用 MSIX 打包

准备好分发应用后,可以使用同一清单将其打包为 MSIX。

准备好包目录

首先,在发布模式下生成应用程序:

flutter build windows

然后,为你的发布文件创建一个目录:

mkdir dist
copy .\build\windows\x64\runner\Release\* .\dist\ -Recurse

Flutter Windows 生成输出包括可执行文件、flutter_windows.dll 和 data 文件夹,所有这些都是必需的。

生成开发证书

在打包之前,您需要一个用于签名的开发证书。 如果尚未生成一个:

winapp cert generate --if-exists skip

签名和打包

现在你可以打包并签名:

winapp pack .\dist --cert .\devcert.pfx

注意:此命令 pack 会自动使用当前目录中的 Package.appxmanifest 命令,并在打包之前将其复制到目标文件夹。

安装证书

在安装 MSIX 包之前,需要信任计算机上的开发证书。 以管理员身份运行此命令(每个证书只需执行此操作一次):

winapp cert install .\devcert.pfx

安装和运行

小窍门

如果在步骤 5 中使用 winapp run ,则可能已在系统上注册包。 首先使用 winapp unregister 删除开发注册,然后安装发布包。

通过双击生成的 .msix 文件或使用 PowerShell 安装包:

Add-AppxPackage .\flutterapp.msix

小窍门

MSIX 文件名包括版本和体系结构(例如)。 flutterapplication1_1.0.0.0_x64.msix 检查目录的确切文件名。 如果需要在代码更改后重新打包,请在 Version 中递增 Package.appxmanifest — Windows需要更高的版本号来更新已安装的包。

提示

  1. 准备好分发后,可以使用证书颁发机构的代码签名证书对 MSIX 进行签名,以便用户无需安装自签名证书。
  2. Azure 受信任签名 服务是安全地管理证书并将登录集成到 CI/CD 管道的好方法。
  3. Microsoft Store将为你签名 MSIX,无需在提交之前进行签名。

后续步骤

  • 通过 winget 分发:将 MSIX 提交到 Windows 程序包管理器 社区存储库
  • 发布到 Microsoft Store:使用 winapp store 提交包
  • 设置 CI/CD:使用 GitHub Action 在流水线中自动打包
  • 探索 Windows API:具有包标识,你现在可以使用通知、设备端 AI 和其他依赖标识的 API