有关完整的工作示例,请查看此存储库中的 Flutter 示例 。
本指南演示如何将 winapp CLI 与 Flutter 应用程序配合使用,以 MSIX 的形式添加包标识和打包应用。
包标识是Windows app模型中的核心概念。 它允许应用程序访问特定的Windows API(例如通知、安全、AI API 等),具有干净的安装/卸载体验等。
标准 Flutter Windows 生成没有包标识。 本指南演示如何添加它进行调试,然后将其打包以供分发。
先决条件
Flutter SDK:按照 官方指南安装 Flutter。
winapp CLI:通过 winget 安装或更新
winappCLI(如果已安装)。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应用程序)
- 设置 SDKs:选择“稳定 SDKs”以下载 Windows 应用 SDK 并生成 C++ 标头(第 6 步需要)
此命令将:
- 创建
Package.appxmanifest— 定义您应用身份的清单 - 创建
Assets文件夹 - MSIX 打包和应用商店提交所需的图标 - 使用Windows 应用 SDK标头和库创建
.winapp文件夹 - 创建一个
winapp.yaml配置文件用于锁定 SDK 版本
可以打开 Package.appxmanifest 以进一步自定义属性,如显示名称、发布者和功能。
使用身份进行调试
若要测试需要标识(如通知)且未完全打包应用的功能,可以使用 winapp run。 这会注册松散布局包(就像真正的 MSIX 安装一样),并在一个步骤中启动应用。 调试不需要证书或签名。
生成应用:
flutter build windows使用标识运行:
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"
然后找到 RegisterPlugins 在 FlutterWindow::OnCreate() 中的调用,并在其后的一行添加 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需要更高的版本号来更新已安装的包。
提示
- 准备好分发后,可以使用证书颁发机构的代码签名证书对 MSIX 进行签名,以便用户无需安装自签名证书。
- Azure 受信任签名 服务是安全地管理证书并将登录集成到 CI/CD 管道的好方法。
- Microsoft Store将为你签名 MSIX,无需在提交之前进行签名。