JavaFX / .NET MAUI / Qt 桌面端框架对比
JavaFX
JavaFX 架构
JavaFX 是 Java 平台上的下一代 GUI 框架,取代了传统的 Swing/AWT。其核心架构基于 Stage / Scene / Node 的层次化场景图模型。
Stage / Scene / Node
- Stage:顶层容器,对应操作系统窗口。每个 JavaFX 应用程序的主入口通过
Application.start(Stage primaryStage)获取主舞台。 - Scene:舞台中的内容容器,承载场景图。每个 Stage 可以切换不同的 Scene。
- Node:场景图中的基本元素,所有 UI 组件(Button、Label、Shape、ImageView)都继承自
Node。 - Parent:可包含子节点的
Node子类,所有布局容器(Pane)都继承自Parent。
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class HelloWorld extends Application {
@Override
public void start(Stage primaryStage) {
Button btn = new Button("Hello JavaFX");
btn.setOnAction(e -> System.out.println("Clicked!"));
StackPane root = new StackPane(btn);
Scene scene = new Scene(root, 600, 400);
primaryStage.setTitle("JavaFX Demo");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}Scene Graph
场景图(Scene Graph)是一个树形结构,根节点为 Root Node,所有渲染和事件处理都基于此树进行遍历。JavaFX 使用 同步渲染管线,每次修改场景图都会触发重新布局和重绘。
Screen
javafx.stage.Screen 类提供显示器的信息获取能力,包括屏幕边界、DPI、主屏识别等:
Screen primary = Screen.getPrimary();
Rectangle2D bounds = primary.getVisualBounds();
double dpi = primary.getDpi();布局 Pane
JavaFX 提供了丰富的布局容器(Pane),每种容器有不同的排列策略:
| 布局容器 | 说明 | 适用场景 |
|---|---|---|
| VBox | 垂直排列子节点 | 纵向表单 |
| HBox | 水平排列子节点 | 工具栏 |
| BorderPane | 上/下/左/右/中 五区域布局 | 主窗口布局 |
| GridPane | 网格布局,支持行列跨度和对齐 | 复杂表单 |
| StackPane | 堆叠布局,子节点层叠排列 | 覆盖层、居中显示 |
| AnchorPane | 锚定布局,子节点通过锚点定位 | 相对定位 |
| FlowPane | 流式布局,子节点按行/列自动换行 | 标签集合 |
| TilePane | 瓦片布局,等大小单元格排列 | 图标网格 |
// BorderPane 示例
BorderPane borderPane = new BorderPane();
borderPane.setTop(new Label("顶部"));
borderPane.setLeft(new Label("左侧"));
borderPane.setCenter(new TextArea("中间区域"));
borderPane.setRight(new Label("右侧"));
borderPane.setBottom(new Label("底部状态栏"));
// GridPane 示例
GridPane grid = new GridPane();
grid.setHgap(10);
grid.setVgap(10);
grid.add(new Label("用户名:"), 0, 0);
grid.add(new TextField(), 1, 0);
grid.add(new Label("密码:"), 0, 1);
grid.add(new PasswordField(), 1, 1);FXML 与 Scene Builder
FXML 声明式 UI
FXML 是基于 XML 的声明式 UI 描述语言,将界面与业务逻辑分离。FXML 文件后缀为 .fxml,可在 JavaFX 应用中被 FXMLLoader 加载:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox xmlns:fx="http://javafx.com/fxml"
fx:controller="com.example.LoginController"
spacing="10" padding="20">
<Label text="用户登录" style="-fx-font-size: 18px;"/>
<HBox spacing="10">
<Label text="用户名:"/>
<TextField fx:id="usernameField"/>
</HBox>
<HBox spacing="10">
<Label text="密码:"/>
<PasswordField fx:id="passwordField"/>
</HBox>
<Button text="登录" onAction="#handleLogin"/>
</VBox>Scene Builder 拖拽
Scene Builder 是 Gluon 提供的可视化布局工具,支持拖拽组件、设置属性、绑定控制器、预览界面,生成的 FXML 文件可直接在代码中使用。
控制器 Controller / @FXML / FXMLLoader
控制器的核心机制:
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.control.TextField;
public class LoginController {
@FXML
private TextField usernameField;
@FXML
private PasswordField passwordField;
@FXML
private void handleLogin() {
String username = usernameField.getText();
String password = passwordField.getText();
System.out.println("登录: " + username);
}
}
// 加载 FXML
FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/LoginView.fxml"));
Parent root = loader.load();
LoginController controller = loader.getController();@FXML标记控制器的字段和方法,使 FXML 能够访问私有成员。FXMLLoader解析 FXML 文件,实例化控件,调用控制器方法。
事件绑定
FXML 支持多种事件绑定方式:
<Button text="点击" onAction="#onButtonClick"/>
<TextField onKeyTyped="#onKeyTyped"/>
<Slider onMouseDragged="#onSliderDrag"/>在控制器中对应方法签名需接收 ActionEvent 等事件类型参数。
自定义组件与 include
FXML 的 <fx:include> 标签可将多个 FXML 文件组合成复杂界面:
<BorderPane xmlns:fx="http://javafx.com/fxml">
<top>
<fx:include source="toolbar.fxml"/>
</top>
<center>
<fx:include source="content-panel.fxml"/>
</center>
</BorderPane>JavaFX 控件
TableView
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
TableView<Person> table = new TableView<>();
TableColumn<Person, String> nameCol = new TableColumn<>("姓名");
nameCol.setCellValueFactory(new PropertyValueFactory<>("name"));
TableColumn<Person, Integer> ageCol = new TableColumn<>("年龄");
ageCol.setCellValueFactory(new PropertyValueFactory<>("age"));
ObservableList<Person> data = FXCollections.observableArrayList(
new Person("张三", 28),
new Person("李四", 32)
);
table.setItems(data);
table.getColumns().addAll(nameCol, ageCol);ListView / TreeView
ListView<String> listView = new ListView<>();
listView.getItems().addAll("选项A", "选项B", "选项C");
TreeView<String> treeView = new TreeView<>();
TreeItem<String> rootItem = new TreeItem<>("根节点");
TreeItem<String> child1 = new TreeItem<>("子节点1");
TreeItem<String> child2 = new TreeItem<>("子节点2");
rootItem.getChildren().addAll(child1, child2);
treeView.setRoot(rootItem);ComboBox / ChoiceBox / DatePicker / ColorPicker
ComboBox<String> combo = new ComboBox<>();
combo.getItems().addAll("选项1", "选项2", "选项3");
combo.setValue("选项1");
DatePicker datePicker = new DatePicker();
datePicker.setValue(LocalDate.now());
ColorPicker colorPicker = new ColorPicker();
colorPicker.setValue(Color.BLUE);Accordion / TitledPane / TabPane / SplitPane
// Accordion
TitledPane pane1 = new TitledPane("面板1", new Label("内容1"));
TitledPane pane2 = new TitledPane("面板2", new Label("内容2"));
Accordion accordion = new Accordion();
accordion.getPanes().addAll(pane1, pane2);
// TabPane
TabPane tabPane = new TabPane();
Tab tab1 = new Tab("标签页1", new Label("页面内容"));
Tab tab2 = new Tab("标签页2", new Label("页面内容"));
tabPane.getTabs().addAll(tab1, tab2);
// SplitPane
SplitPane splitPane = new SplitPane();
splitPane.getItems().addAll(new TextArea("左"), new TextArea("右"));
splitPane.setDividerPositions(0.5);WebView / MediaView
// WebView - 内嵌 Chromium 浏览器
WebView webView = new WebView();
WebEngine engine = webView.getEngine();
engine.load("https://example.com");
// MediaView - 播放视频
Media media = new Media("file:///path/to/video.mp4");
MediaPlayer mediaPlayer = new MediaPlayer(media);
MediaView mediaView = new MediaView(mediaPlayer);
mediaPlayer.play();Chart
JavaFX 内置 javafx.scene.chart 图表组件,支持丰富的可视化数据展示:
// LineChart
ObservableList<XYChart.Series<Number, Number>> lineData = FXCollections.observableArrayList();
XYChart.Series<Number, Number> series = new XYChart.Series<>();
series.setName("2024 年");
series.getData().add(new XYChart.Data<>(1, 23));
series.getData().add(new XYChart.Data<>(2, 45));
series.getData().add(new XYChart.Data<>(3, 67));
lineData.add(series);
NumberAxis xAxis = new NumberAxis();
NumberAxis yAxis = new NumberAxis();
LineChart<Number, Number> lineChart = new LineChart<>(xAxis, yAxis);
lineChart.setData(lineData);
// BarChart
CategoryAxis barX = new CategoryAxis();
NumberAxis barY = new NumberAxis();
BarChart<String, Number> barChart = new BarChart<>(barX, barY);
XYChart.Series<String, Number> barSeries = new XYChart.Series<>();
barSeries.setName("销售额");
barSeries.getData().add(new XYChart.Data<>("一月", 12000));
barSeries.getData().add(new XYChart.Data<>("二月", 15000));
barChart.getData().add(barSeries);
// PieChart
ObservableList<PieChart.Data> pieData = FXCollections.observableArrayList(
new PieChart.Data("苹果", 30),
new PieChart.Data("香蕉", 20),
new PieChart.Data("橘子", 25)
);
PieChart pieChart = new PieChart(pieData);
// ScatterChart
ScatterChart<Number, Number> scatterChart = new ScatterChart<>(new NumberAxis(), new NumberAxis());
// AreaChart
AreaChart<Number, Number> areaChart = new AreaChart<>(new NumberAxis(), new NumberAxis());CSS 样式与图表
JavaFX CSS
JavaFX 使用 CSS 语法为 UI 组件设置样式,与 Web CSS 类似但有其专属属性。样式表通过 Scene.getStylesheets().add() 加载:
scene.getStylesheets().add("/styles/theme.css");CSS 示例:
/* theme.css */
.root {
-fx-font-family: "Microsoft YaHei";
-fx-background-color: #f5f5f5;
}
.button {
-fx-background-color: #2196F3;
-fx-text-fill: white;
-fx-font-size: 14px;
-fx-padding: 8 16 8 16;
-fx-background-radius: 4;
}
.button:hover {
-fx-background-color: #1976D2;
}
.button:pressed {
-fx-background-color: #1565C0;
}
.table-view {
-fx-border-color: #e0e0e0;
}
.table-row-cell:selected {
-fx-background-color: #BBDEFB;
}
.label {
-fx-text-fill: #333333;
}
.text-field {
-fx-border-color: #bdbdbd;
-fx-border-radius: 4;
-fx-padding: 6 10 6 10;
}伪类与选择器属性
JavaFX CSS 支持多种选择器:
- 类选择器:
.button { ... } - ID 选择器:
#myButton { ... } - 伪类:
:hover,:pressed,:selected,:focused,:disabled - 组合选择器:
.button:hover,.table-row-cell:odd - 子选择器:
.vbox > .button
图表样式定制
.chart {
-fx-background-color: white;
}
.chart-title {
-fx-font-size: 16px;
-fx-text-fill: #333;
}
.chart-legend {
-fx-background-color: transparent;
}
.chart-bar {
-fx-bar-fill: #2196F3;
}
.default-color0.chart-bar { -fx-bar-fill: #FF5722; }
.default-color1.chart-bar { -fx-bar-fill: #4CAF50; }
.default-color2.chart-bar { -fx-bar-fill: #2196F3; }
.chart-pie-label-line { -fx-stroke: #666; }
.chart-pie-label { -fx-fill: #333; }
.chart-line-symbol { -fx-background-radius: 4px; }
.chart-series-line { -fx-stroke-width: 2px; }主题修改
通过修改 -fx-base 等根变量可快速切换主题:
/* 深色主题 */
.root {
-fx-base: #2b2b2b;
-fx-background: #3c3f41;
-fx-control-inner-background: #45474a;
-fx-text-background-color: #bbbbbb;
-fx-text-fill: #bbbbbb;
}JavaFX 打包
jlink / jpackage / 模块化
jlink 是 JDK 9 引入的工具,用于创建包含应用程序及其依赖模块的自定义运行时映像。它仅打包所需模块,大幅减小体积。
jpackage 是 JDK 14 引入的打包工具,将 JavaFX 应用打包为原生安装包:
# 编译模块化应用
javac -d out --module-source-path src -m com.example.app
# 创建模块化运行时映像
jlink --module-path %JAVA_HOME%\jmods;out \
--add-modules com.example.app,javafx.controls,javafx.fxml \
--output custom-runtime
# 打包为 Windows 安装程序 (exe/msi)
jpackage --type msi \
--name "MyApp" \
--module com.example.app/com.example.Main \
--runtime-image custom-runtime \
--app-version 1.0.0 \
--vendor "MyCompany"原生安装包类型
| 平台 | 格式 | 说明 |
|---|---|---|
| Windows | exe, msi | 使用 WiX Toolset |
| macOS | dmg, pkg | 生成 .app 捆绑包 |
| Linux | deb, rpm | 使用 fakeroot |
JDK 版本
JavaFX 在 JDK 11 后从 JDK 中分离,成为独立模块(OpenJFX),需要额外引入依赖:
<!-- Maven 依赖 -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>22</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>22</version>
</dependency>GraalVM 支持
GraalVM 支持将 JavaFX 应用编译为原生映像(Native Image),实现更快的启动时间和更小的内存占用。但需要注意 JavaFX 与 GraalVM 的兼容性限制:
# GraalVM 原生编译
native-image --module-path %JAVA_HOME%\javafx-sdk\lib \
--add-modules javafx.controls,javafx.fxml \
--module com.example.app/com.example.Main \
--enable-preview.NET MAUI
MAUI 架构
.NET MAUI(Multi-platform App UI)是微软推出的跨平台 UI 框架,基于 .NET 6+,使用单个项目同时构建 Windows、macOS、Android、iOS 应用。
.NET 6+ / 单项目多平台
MAUI 的核心思想是 单项目结构,通过目标框架(Target Framework)区分平台,在同一个 .csproj 文件中管理所有平台的代码:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks>
<OutputType>Exe</OutputType>
<UseMaui>true</UseMaui>
</PropertyGroup>
</Project>Handler
Handler 是 MAUI 的跨平台抽象机制,替代了 Xamarin.Forms 中的 Renderer。每个虚拟控件映射到不同平台的原生控件:
MAUI Button → Windows: Button (WinUI 3)
→ macOS: NSButton
→ Android: AppCompatButton
→ iOS: UIButton自定义 Handler 示例:
public class CustomEntryHandler : ViewHandler<Entry, PlatformEntry>
{
protected override PlatformEntry CreatePlatformView()
{
return new PlatformEntry(Context);
}
protected override void ConnectHandler(PlatformEntry platformView)
{
base.ConnectHandler(platformView);
// 连接事件
}
}跨平台 API
MAUI 提供了 DeviceInfo、Preferences、FileSystem 等跨平台 API,屏蔽平台差异:
// 设备信息
var model = DeviceInfo.Model;
var platform = DeviceInfo.Platform;
var idiom = DeviceInfo.Idiom;
var version = DeviceInfo.VersionString;
// 文件系统
string cacheDir = FileSystem.CacheDirectory;
string appDataDir = FileSystem.AppDataDirectory;WebView / Border / GraphicsView / Shadow
<!-- WebView -->
<WebView x:Name="MyWebView"
Source="https://example.com"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"/>
<!-- Border (圆角容器) -->
<Border StrokeShape="RoundRectangle 12"
Stroke="#2196F3"
StrokeThickness="2"
Padding="16">
<Label Text="圆角边框内容"/>
</Border>
<!-- Shadow -->
<Border StrokeShape="RoundRectangle 8" BackgroundColor="White">
<Border.Shadow>
<Shadow Brush="#22000000"
Offset="0,4"
Radius="12"/>
</Border.Shadow>
<Label Text="带阴影的卡片" Margin="16"/>
</Border>FlexLayout
FlexLayout 实现了 CSS Flexbox 布局模型,适合复杂自适应布局:
<FlexLayout Wrap="Wrap"
JustifyContent="SpaceEvenly"
AlignItems="Center"
Direction="Row">
<Label Text="项目1" WidthRequest="100"/>
<Label Text="项目2" WidthRequest="150"/>
<Label Text="项目3" WidthRequest="120"/>
</FlexLayout>平台特定代码与条件编译
MAUI 通过 条件编译 和 Platforms 目录 实现平台特定逻辑:
// 条件编译
#if ANDROID
// Android 特定代码
#elif IOS
// iOS 特定代码
#elif WINDOWS
// Windows 特定代码
#endif
// 使用 Platform 类
string version = Microsoft.Maui.Devices.DeviceInfo.Current.Platform.ToString();Platforms 目录 结构:
MyApp/
├── Platforms/
│ ├── Android/
│ │ ├── AndroidManifest.xml
│ │ ├── MainApplication.cs
│ │ └── MainActivity.cs
│ ├── iOS/
│ │ ├── AppDelegate.cs
│ │ ├── Info.plist
│ │ └── Program.cs
│ ├── MacCatalyst/
│ │ ├── AppDelegate.cs
│ │ └── Info.plist
│ └── Windows/
│ ├── App.xaml
│ ├── App.xaml.cs
│ └── Package.appxmanifestXAML UI 开发
XAML 语法
XAML(Extensible Application Markup Language)是 MAUI 的声明式 UI 语言,与 FXML 类似但更成熟:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyApp.MainPage"
Title="主页">
<VerticalStackLayout Padding="20" Spacing="15">
<Label Text="欢迎使用 MAUI"
FontSize="24"
FontAttributes="Bold"
HorizontalOptions="Center"/>
<Entry x:Name="NameEntry" Placeholder="请输入姓名"/>
<Button Text="提交"
Clicked="OnSubmitClicked"/>
</VerticalStackLayout>
</ContentPage>代码隐藏
每个 XAML 文件对应一个代码隐藏文件(Code-behind):
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
private async void OnSubmitClicked(object sender, EventArgs e)
{
string name = NameEntry.Text;
await DisplayAlert("提示", $"你好, {name}!", "确定");
}
}数据绑定 Binding
数据绑定是 XAML 的核心机制,通过 Binding 标记扩展实现视图与数据的同步:
<Label Text="{Binding UserName}"/>
<Entry Text="{Binding UserName, Mode=TwoWay}"/>
<Button Command="{Binding SubmitCommand}"/>MVVM 模式
MAUI 原生支持 MVVM 模式,通过 INotifyPropertyChanged 实现视图模型与视图的交互:
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
public class MainViewModel : INotifyPropertyChanged
{
private string _userName;
public string UserName
{
get => _userName;
set
{
if (_userName != value)
{
_userName = value;
OnPropertyChanged();
}
}
}
public ICommand SubmitCommand { get; }
public MainViewModel()
{
SubmitCommand = new Command(async () =>
{
await Application.Current.MainPage.DisplayAlert("提示", $"你好, {UserName}", "确定");
});
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}在 XAML 中绑定 ViewModel:
<ContentPage xmlns:vm="clr-namespace:MyApp.ViewModels"
x:Class="MyApp.MainPage">
<ContentPage.BindingContext>
<vm:MainViewModel/>
</ContentPage.BindingContext>
<!-- 使用绑定 -->
</ContentPage>命令 ICommand / RelayCommand
.NET Community Toolkit 提供了 RelayCommand 简化命令定义:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public partial class LoginViewModel : ObservableObject
{
[ObservableProperty]
private string username;
[ObservableProperty]
private string password;
[RelayCommand]
private async Task LoginAsync()
{
// 登录逻辑
await Task.Delay(1000);
}
}触发器与行为
<!-- 触发器 -->
<Entry Placeholder="输入邮箱">
<Entry.Triggers>
<Trigger TargetType="Entry"
Binding="{Binding Source={x:Reference EmailEntry},
Path=Text.Length}"
Value="0">
<Setter Property="BackgroundColor" Value="#FFF3E0"/>
</Trigger>
</Entry.Triggers>
</Entry>
<!-- 行为 -->
<Entry Placeholder="数字输入">
<Entry.Behaviors>
<local:NumericValidationBehavior/>
</Entry.Behaviors>
</Entry>动画
MAUI 支持声明式和代码动画:
// 代码动画
await myLabel.FadeTo(0, 500); // 淡出
await myLabel.RotateTo(360, 1000); // 旋转
await myLabel.ScaleTo(1.5, 300); // 缩放
await myLabel.TranslateTo(100, 0, 500); // 平移控件模板与 DataTemplate
<!-- DataTemplate 用于列表项 -->
<CollectionView ItemsSource="{Binding Products}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Frame Margin="8" Padding="12" CornerRadius="8">
<VerticalStackLayout Spacing="4">
<Label Text="{Binding Name}" FontSize="16" FontAttributes="Bold"/>
<Label Text="{Binding Price, StringFormat='¥{0:F2}'}"
TextColor="#FF5722"
FontSize="14"/>
</VerticalStackLayout>
</Frame>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>MAUI 控件
CollectionView / CarouselView
<!-- CollectionView - 可滚动列表 -->
<CollectionView ItemsSource="{Binding Items}"
SelectionMode="Single"
SelectionChanged="OnSelectionChanged">
<CollectionView.ItemsLayout>
<GridItemsLayout Orientation="Vertical"
Span="2"
HorizontalItemSpacing="8"
VerticalItemSpacing="8"/>
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<Frame Padding="10" CornerRadius="8">
<Label Text="{Binding Title}"/>
</Frame>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<!-- CarouselView - 轮播图 -->
<CarouselView ItemsSource="{Binding Images}"
Loop="True"
PeekAreaInsets="20">
<CarouselView.ItemTemplate>
<DataTemplate>
<Image Source="{Binding Url}"
Aspect="AspectFill"/>
</DataTemplate>
</CarouselView.ItemTemplate>
</CarouselView>RefreshView / SwipeView / IndicatorView
<!-- RefreshView 下拉刷新 -->
<RefreshView IsRefreshing="{Binding IsRefreshing}"
Command="{Binding RefreshCommand}">
<CollectionView ItemsSource="{Binding Items}"/>
</RefreshView>
<!-- SwipeView 滑动操作 -->
<SwipeView>
<SwipeView.RightItems>
<SwipeItems Mode="Execute">
<SwipeItem Text="删除"
BackgroundColor="Red"
Command="{Binding DeleteCommand}"/>
</SwipeItems>
</SwipeView.RightItems>
<Frame Padding="16">
<Label Text="向左滑动删除"/>
</Frame>
</SwipeView>
<!-- IndicatorView 指示器 -->
<VerticalStackLayout>
<CarouselView x:Name="ImageCarousel"
ItemsSource="{Binding Images}"/>
<IndicatorView Source="{x:Reference ImageCarousel}"
IndicatorColor="#BDBDBD"
SelectedIndicatorColor="#2196F3"/>
</VerticalStackLayout>Path / GraphicsView
<!-- Path 矢量图形 -->
<Path Data="M0,0 L100,0 L50,100 Z"
Fill="#2196F3"
Stroke="#1565C0"
StrokeThickness="2"/>
<!-- GraphicsView 自定义绘制 -->
<GraphicsView x:Name="Canvas"
HorizontalOptions="Fill"
VerticalOptions="Fill">
<GraphicsView.Drawable>
<local:MyDrawable/>
</GraphicsView.Drawable>
</GraphicsView>Map / WebView / MediaElement
<!-- Map -->
<Map IsShowingUser="True"
x:Name="MyMap">
<Map.Pins>
<Pin Label="当前位置"
Coordinate="31.2304,121.4737"/>
</Map.Pins>
</Map>
<!-- MediaElement -->
<MediaElement Source="https://example.com/video.mp4"
ShouldAutoPlay="False"
CanSeek="True"/>平台特性调用
Essentials API
MAUI Essentials 提供统一的跨平台设备 API:
// 传感器
var accelerometer = Accelerometer.Default;
if (accelerometer.IsSupported)
{
accelerometer.ReadingChanged += (s, e) =>
{
var data = e.Reading.Acceleration;
};
accelerometer.Start();
}
// 地理位置
var location = await Geolocation.GetLastKnownLocationAsync();
if (location != null)
{
double lat = location.Latitude;
double lng = location.Longitude;
}
// 偏好设置
Preferences.Set("user_token", "abc123");
string token = Preferences.Get("user_token", "");
// 文件系统
string path = FileSystem.AppDataDirectory;
// 媒体选取
var photo = await MediaPicker.CapturePhotoAsync();
if (photo != null)
{
using var stream = await photo.OpenReadAsync();
}
// 短信与电话
await Sms.ComposeAsync(new SmsMessage("你好", "13800138000"));
PhoneDialer.Open("13800138000");
// 分享
await Share.RequestAsync(new ShareTextRequest
{
Text = "分享内容",
Title = "分享标题"
});
// 认证 - 生物识别
var result = await BiometricAuthentication.Default.AuthenticateAsync(
new AuthenticationRequest("验证", "请验证指纹"));
// 设备信息
string deviceName = DeviceInfo.Name;
string deviceType = DeviceInfo.DeviceType.ToString();平台调用
通过 Platform 类或条件编译调用平台特定 API:
#if ANDROID
using Android.Content;
var context = Android.App.Application.Context;
var intent = new Intent(context, typeof(MyActivity));
context.StartActivity(intent);
#elif IOS
using UIKit;
var controller = new UIViewController();
controller.View.BackgroundColor = UIColor.Red;
#endif权限管理
public async Task CheckAndRequestPermissions()
{
var status = await Permissions.CheckStatusAsync<Permissions.LocationWhenInUse>();
if (status != PermissionStatus.Granted)
{
status = await Permissions.RequestAsync<Permissions.LocationWhenInUse>();
}
}// 常用权限类型:LocationWhenInUse, Camera, Microphone, Photos, StorageRead, StorageWrite
MAUI 打包
| 平台 | 打包格式 | 工具/要求 |
|---|---|---|
| Windows | MSIX | Windows Application Packaging Project |
| macOS | DMG | 需要 Apple 开发者证书 |
| Android | APK / AAB | Android SDK, keystore 签名 |
| iOS | IPA | Apple 开发者账号, Xcode |
Windows MSIX
dotnet publish -f net8.0-windows10.0.19041.0 -c ReleaseAndroid APK 签名
dotnet publish -f net8.0-android -c Release \
-p:AndroidKeyStore=true \
-p:AndroidSigningKeyStore=myapp.keystore \
-p:AndroidSigningKeyAlias=mykey \
-p:AndroidSigningKeyPass=password \
-p:AndroidSigningStorePass=passwordApple 证书 (.cer / .p12)
签名流程:
- 在 Apple Developer Center 创建 App ID 和证书
- 下载
.cer文件导入钥匙串 - 导出为
.p12分发证书 - 配置签名信息到项目
自动构建 / Azure Pipeline
# azure-pipelines.yml
trigger:
- main
pool:
vmImage: 'windows-latest'
variables:
- group: 'MAUI Build Secrets'
steps:
- task: DotNetCoreCLI@2
displayName: 'Build MAUI'
inputs:
command: 'build'
arguments: '-f net8.0-android -c Release'
- task: DotNetCoreCLI@2
displayName: 'Publish APK'
inputs:
command: 'publish'
arguments: '-f net8.0-android -c Release /p:AndroidKeyStore=true /p:AndroidSigningKeyAlias=$(SignAlias) /p:AndroidSigningKeyPass=$(SignPass)'Qt
Qt 模块体系
Qt 是由 Qt Company 维护的 C++ 跨平台框架,拥有丰富的模块体系:
| 模块 | 说明 | 用途 |
|---|---|---|
| QtWidgets | 经典桌面控件模块 | 传统桌面应用 |
| QtQuick / QtQml | 声明式 UI 引擎 | 现代流畅 UI |
| QtNetwork | 网络通信 | TCP/UDP/HTTP/SSL |
| QtWebEngine | 基于 Chromium 的 Web 引擎 | 内嵌浏览器 |
| QtCharts | 图表组件 | 数据可视化 |
| QtSql | 数据库访问 | SQLite/MySQL/PostgreSQL |
| QtMultimedia | 多媒体播放与录制 | 音视频 |
| QtSvg | SVG 矢量图渲染 | 图标与图形 |
| QtPdf | PDF 文档渲染 | 文档查看 |
| QtBluetooth | 蓝牙通信 | 低功耗蓝牙 |
| QtNfc | NFC 近场通信 | 标签读取 |
| QtSensors | 传感器接口 | 加速度计/陀螺仪 |
| QtSerialPort | 串口通信 | 工业通信 |
| QtMqtt | MQTT 客户端 | IoT 消息传输 |
| QtGrpc | gRPC 支持 | 微服务通信 |
| QtProtobuf | Protocol Buffers | 序列化 |
QWidget 开发
QMainWindow / QWidget / QDialog
#include <QMainWindow>
#include <QWidget>
#include <QDialog>
#include <QPushButton>
#include <QLabel>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr)
: QMainWindow(parent)
{
setWindowTitle("Qt 应用");
resize(800, 600);
// 中心部件
auto *centralWidget = new QWidget(this);
setCentralWidget(centralWidget);
}
};
// 模态对话框
void showDialog()
{
QDialog dialog;
dialog.setWindowTitle("对话框");
dialog.resize(300, 200);
dialog.exec(); // 模态
}布局 QVBoxLayout / QHBoxLayout / QGridLayout / QFormLayout
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QFormLayout>
#include <QPushButton>
#include <QLabel>
#include <QLineEdit>
// 垂直布局
QVBoxLayout *vbox = new QVBoxLayout;
vbox->addWidget(new QLabel("标签"));
vbox->addWidget(new QPushButton("按钮"));
vbox->addStretch(); // 弹性空间
// 水平布局
QHBoxLayout *hbox = new QHBoxLayout;
hbox->addWidget(new QPushButton("左"));
hbox->addWidget(new QPushButton("中"));
hbox->addWidget(new QPushButton("右"));
// 网格布局
QGridLayout *grid = new QGridLayout;
grid->addWidget(new QLabel("姓名:"), 0, 0);
grid->addWidget(new QLineEdit, 0, 1);
grid->addWidget(new QLabel("邮箱:"), 1, 0);
grid->addWidget(new QLineEdit, 1, 1);
// 表单布局
QFormLayout *form = new QFormLayout;
form->addRow("用户名:", new QLineEdit);
form->addRow("密码:", new QLineEdit);信号槽 connect / Signal / Slot / Lambda
信号槽是 Qt 的核心机制,用于对象间通信:
#include <QPushButton>
#include <QSlider>
#include <QLabel>
class MyWidget : public QWidget
{
Q_OBJECT
signals:
void valueChanged(int newValue);
public slots:
void onButtonClick()
{
qDebug() << "按钮被点击";
}
void onSliderMoved(int value)
{
qDebug() << "滑块值:" << value;
emit valueChanged(value); // 发射信号
}
void setupConnections()
{
// 传统方式
connect(button, &QPushButton::clicked,
this, &MyWidget::onButtonClick);
// Lambda 方式
connect(slider, &QSlider::valueChanged,
this, [this](int value) {
label->setText(QString("值: %1").arg(value));
});
// Lambda 简化
connect(button, &QPushButton::clicked, this, [this]() {
qDebug() << "Lambda 信号处理";
});
}
private:
QPushButton *button;
QSlider *slider;
QLabel *label;
};事件 event 重写与事件过滤器
#include <QEvent>
#include <QKeyEvent>
class CustomWidget : public QWidget
{
protected:
// 重写事件处理器
void keyPressEvent(QKeyEvent *event) override
{
if (event->key() == Qt::Key_Escape) {
close();
} else if (event->key() == Qt::Key_Return) {
onConfirm();
}
QWidget::keyPressEvent(event);
}
void paintEvent(QPaintEvent *event) override
{
QPainter painter(this);
painter.setPen(Qt::blue);
painter.setBrush(Qt::lightGray);
painter.drawRect(rect().adjusted(10, 10, -10, -10));
painter.drawText(rect(), Qt::AlignCenter, "自定义绘制");
}
// 事件过滤器
bool eventFilter(QObject *watched, QEvent *event) override
{
if (watched == targetWidget && event->type() == QEvent::MouseButtonPress) {
qDebug() << "目标部件被点击(事件过滤器捕获)";
return true; // 事件已处理
}
return QWidget::eventFilter(watched, event);
}
};
// 安装事件过滤器
CustomWidget widget;
widget.installEventFilter(&filterObj);QML 与 QtQuick
QML 语法
QML(Qt Markup Language)是声明式语言,用于描述 UI 结构和行为:
import QtQuick 2.15
import QtQuick.Controls 2.15
ApplicationWindow {
visible: true
width: 800
height: 600
title: "QML 应用"
Rectangle {
anchors.centerIn: parent
width: 200
height: 200
color: "lightblue"
radius: 10
Text {
anchors.centerIn: parent
text: "Hello QML"
font.pixelSize: 18
color: "navy"
}
MouseArea {
anchors.fill: parent
onClicked: parent.color = "lightgreen"
}
}
}属性、信号、行为、动画
import QtQuick 2.15
Rectangle {
id: rect
width: 100
height: 100
color: "red"
// 属性
property int count: 0
// 自定义信号
signal reached(int value)
// 属性动画
NumberAnimation on opacity {
from: 1.0
to: 0.3
duration: 500
loops: Animation.Infinite
easing.type: Easing.InOutQuad
}
// 行为动画
Behavior on color {
ColorAnimation { duration: 300 }
}
// 状态
states: [
State {
name: "hovered"
PropertyChanges { target: rect; color: "blue"; scale: 1.1 }
},
State {
name: "clicked"
PropertyChanges { target: rect; color: "green"; scale: 0.9 }
}
]
// 过渡
transitions: [
Transition {
from: "*"; to: "*"
PropertyAnimation { properties: "color,scale"; duration: 200 }
}
]
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: rect.state = "hovered"
onExited: rect.state = ""
onClicked: {
rect.count++
rect.reached(rect.count)
}
}
}Model-View Delegate
import QtQuick 2.15
import QtQuick.Controls 2.15
// ListView
ListView {
width: 300
height: 400
model: ListModel {
ListElement { name: "苹果"; price: 5.5 }
ListElement { name: "香蕉"; price: 3.2 }
ListElement { name: "橘子"; price: 4.8 }
}
delegate: Rectangle {
width: parent.width
height: 50
color: index % 2 === 0 ? "#F5F5F5" : "white"
border.color: "#E0E0E0"
Row {
anchors.verticalCenter: parent.verticalCenter
x: 16
spacing: 12
Text { text: name; font.pixelSize: 16 }
Text {
text: "¥" + price.toFixed(2)
color: "#FF5722"
font.pixelSize: 14
}
}
MouseArea {
anchors.fill: parent
onClicked: console.log("选中:", name)
}
}
}
// GridView
GridView {
anchors.fill: parent
cellWidth: 120
cellHeight: 120
model: myModel
delegate: Rectangle {
width: 100
height: 100
color: model.color
Text {
anchors.centerIn: parent
text: model.label
}
}
}
// TableView (Qt Quick)
TableView {
anchors.fill: parent
model: tableModel
delegate: Text {
text: display
padding: 8
}
}Particles / ShaderEffect
import QtQuick 2.15
import QtQuick.Particles 2.15
// 粒子系统
ParticleSystem {
id: particleSystem
ImageParticle {
source: "qrc:///particle.png"
colorVariation: 0.5
rotation: 45
}
Emitter {
anchors.centerIn: parent
emitRate: 50
lifeSpan: 2000
velocity: PointDirection { y: -100; yVariation: 50 }
acceleration: PointDirection { y: 50 }
size: 16
}
}
// ShaderEffect
ShaderEffect {
anchors.fill: parent
property real wave: 0.0
NumberAnimation on wave {
from: 0; to: 2 * Math.PI
duration: 3000
loops: Animation.Infinite
}
fragmentShader: "
varying highp vec2 qt_TexCoord0;
uniform highp float wave;
uniform sampler2D source;
void main() {
highp vec2 pos = qt_TexCoord0;
pos.y += sin(pos.x * 20.0 + wave) * 0.05;
gl_FragColor = texture2D(source, pos);
}
"
}Qt Quick Controls
Qt Quick Controls 2 提供了现代、流畅的控件集,适用于触控和桌面:
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
ApplicationWindow {
visible: true
width: 1024
height: 768
title: "Qt Quick Controls"
menuBar: MenuBar {
Menu {
title: "文件"
Action { text: "新建"; shortcut: "Ctrl+N" }
Action { text: "打开"; shortcut: "Ctrl+O" }
MenuSeparator {}
Action { text: "退出"; shortcut: "Ctrl+Q" }
}
Menu {
title: "帮助"
Action { text: "关于" }
}
}
header: ToolBar {
RowLayout {
anchors.fill: parent
ToolButton { text: "菜单" }
Label { text: "应用标题"; elide: Label.ElideRight; Layout.fillWidth: true }
ToolButton { text: "搜索" }
}
}
Drawer {
id: drawer
width: 250
height: parent.height
ListView {
anchors.fill: parent
model: ["首页", "设置", "关于"]
delegate: ItemDelegate {
text: modelData
width: parent.width
onClicked: drawer.close()
}
}
}
StackView {
id: stackView
anchors.fill: parent
initialItem: homePage
// 页面间切换
push("DetailPage.qml")
pop()
}
SwipeView {
id: swipeView
anchors.fill: parent
currentIndex: tabBar.currentIndex
Page { Label { text: "页面1" } }
Page { Label { text: "页面2" } }
Page { Label { text: "页面3" } }
}
footer: TabBar {
id: tabBar
currentIndex: swipeView.currentIndex
TabButton { text: "首页" }
TabButton { text: "发现" }
TabButton { text: "我的" }
}
}Qt Quick Controls 与 QWidget 对比:
| 维度 | QWidget | Qt Quick Controls |
|---|---|---|
| 语言 | C++ | QML + JavaScript |
| 渲染 | 基于 QPainter (CPU) | 基于场景图 (GPU 加速) |
| 界面风格 | 原生平台风格 | 自定义风格为主 |
| 开发效率 | 较低 | 较高,热重载支持 |
| 适合场景 | 传统桌面应用 | 现代流畅 UI、触控应用 |
| 动画支持 | 有限 | 丰富的内置动画 |
| 学习曲线 | 较陡 | 相对平缓 |
Qt 跨平台构建
CMake / qmake
Qt 6 推荐使用 CMake,Qt 5 支持 qmake:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(MyApp VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
find_package(Qt6 REQUIRED COMPONENTS
Widgets
Quick
QuickControls2
Network
Sql
)
add_executable(MyApp
main.cpp
mainwindow.cpp
resources.qrc
)
target_link_libraries(MyApp PRIVATE
Qt6::Widgets
Qt6::Quick
Qt6::QuickControls2
Qt6::Network
Qt6::Sql
)# qmake .pro 文件
QT += widgets quick quickcontrols2 network sql
CONFIG += c++17
SOURCES += main.cpp mainwindow.cpp
RESOURCES += resources.qrc
TARGET = MyApp套件 Kit 与编译
Qt Creator 通过 Kit 管理不同的编译配置:
Kit = Qt 版本 + 编译器 + 调试器 + 构建系统 + 设备类型
例如: Qt 6.5.3 + MSVC 2022 + Windows Debugger + CMake + Desktop命令行编译:
# 生成构建文件
cmake -B build -S . \
-G "Visual Studio 17 2022" \
-DCMAKE_PREFIX_PATH=C:/Qt/6.5.3/msvc2022_64
# 编译
cmake --build build --config Release交叉编译
# Android 交叉编译
cmake -B build-android -S . \
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a \
-DANDROID_PLATFORM=android-24 \
-DCMAKE_PREFIX_PATH=$QT_DIR/android_arm64_v8a
# 嵌入式 Linux 编译
cmake -B build-arm -S . \
-DCMAKE_TOOLCHAIN_FILE=/opt/arm-linux-gnueabihf.cmake \
-DCMAKE_PREFIX_PATH=$QT_DIR/raspberrypi4Android 部署
# 生成 APK
cmake --build build-android --target apk
# 或使用 Qt Creator 直接部署到设备Qt for MCU
Qt for MCU 是面向微控制器的轻量级版本,适用于资源受限的嵌入式设备:
- 支持 MCU 上的 QML UI
- 低内存占用(数百 KB)
- 无操作系统依赖
许可证(LGPL / GPL / 商业版)
| 许可证 | 要求 | 适用场景 |
|---|---|---|
| LGPL | 动态链接,用户可替换 Qt 库 | 商业闭源应用(动态链接) |
| GPL | 应用必须开源 | 开源项目 |
| 商业版 | 静态链接,闭源发布,技术支持 | 闭源商业应用(静态链接) |
三方框架对比
开发语言
| 框架 | 主语言 | 辅助语言 | 类型系统 |
|---|---|---|---|
| JavaFX | Java | FXML(声明式 UI) | 静态强类型 |
| .NET MAUI | C# | XAML(声明式 UI) | 静态强类型 |
| Qt | C++ / QML | JavaScript(QML 中) | 静态强类型 + 动态 |
UI 定义方式
// JavaFX - 代码定义 UI
Button btn = new Button("提交");
btn.setStyle("-fx-background-color: #2196F3;");
btn.setOnAction(e -> handleSubmit());<!-- JavaFX - FXML 声明式 -->
<Button text="提交" style="-fx-background-color: #2196F3;" onAction="#handleSubmit"/>// MAUI - 代码定义 UI
var btn = new Button { Text = "提交", BackgroundColor = Colors.Blue };
btn.Clicked += OnSubmit;<!-- MAUI - XAML 声明式 -->
<Button Text="提交" BackgroundColor="#2196F3" Clicked="OnSubmit"/>// Qt Widgets - C++ 定义 UI
QPushButton *btn = new QPushButton("提交");
btn->setStyleSheet("background-color: #2196F3;");
connect(btn, &QPushButton::clicked, this, &MyClass::onSubmit);// Qt Quick - QML 声明式
Button {
text: "提交"
background: Rectangle { color: "#2196F3" }
onClicked: console.log("提交")
}性能对比
| 维度 | JavaFX | .NET MAUI | Qt |
|---|---|---|---|
| 启动时间 | 中等(需 JVM 预热) | 中等(.NET JIT) | 快(原生编译) |
| 内存占用 | 较高(JVM 堆) | 中等 | 低 |
| 渲染性能 | 良好(Prism 引擎) | 良好(平台原生) | 优秀(直接调用 OpenGL/Vulkan) |
| CPU 密集型任务 | 优秀(Java 生态) | 优秀(.NET 生态) | 优秀(C++ 原生) |
| GPU 加速 | 支持 | 支持 | 原生支持 |
| 包体大小 | 较大(含 JRE) | 中等 | 较小(静态链接) |
学习曲线对比
陡峭度: Qt (C++) > JavaFX ≈ .NET MAUI > Qt (QML)
| | | | |
| C++ 模板/指针 FXML 学习 XAML 学习 QML 简洁语法
| 信号槽复杂 布局 API MVVM 模式 声明式直观
| 内存管理 CSS 样式 数据绑定 热重载
v 编译模型 平台差异 平台特定代码生态与社区
| 维度 | JavaFX | .NET MAUI | Qt |
|---|---|---|---|
| 社区规模 | 中等 | 较大(.NET 生态) | 大(嵌入式/IoT 强) |
| 第三方库 | 较少 | 丰富(NuGet) | 丰富(C/C++ 库) |
| 工具支持 | Scene Builder | Visual Studio / VS Code | Qt Creator / VS |
| 文档质量 | 一般 | 良好 | 优秀 |
| 企业采用 | 传统企业级 | .NET 企业 | 嵌入式/汽车/工业 |
| 版本更新 | 较慢 | 快速(.NET 节奏) | 稳定(年度大版本) |
跨平台支持度
| 平台 | JavaFX | .NET MAUI | Qt |
|---|---|---|---|
| Windows | 支持 | 支持 (WinUI 3) | 支持 |
| macOS | 支持 | 支持 | 支持 |
| Linux | 支持 | 不支持 | 支持 |
| Android | 支持(Gluon 方案) | 支持 | 支持 |
| iOS | 支持(Gluon 方案) | 支持 | 支持(商业版) |
| Web | 不支持 | 不支持 | 不支持(Qt for WebAssembly 实验) |
| 嵌入式 | 不支持 | 不支持 | 全面支持(Qt for MCU) |
| 跨平台一致性 | 高(自绘 UI) | 中等(原生控件) | 高(自绘引擎) |
企业应用场景
| 场景 | 推荐框架 | 原因 |
|---|---|---|
| 金融交易终端 | JavaFX | Java 企业生态成熟,图表丰富 |
| Windows 桌面工具 | .NET MAUI | Windows 平台深度集成,MSIX 打包 |
| 嵌入式工业 HMI | Qt | 性能优异,硬件兼容性强 |
| 跨平台移动-桌面统一 | .NET MAUI | 单一项目覆盖所有平台 |
| 汽车仪表盘 / IVI | Qt | Qt for MCU, Qt Safe, 功能安全认证 |
| 医疗影像系统 | JavaFX / Qt | 高性能渲染,成熟的企业案例 |
| 内部管理系统 | JavaFX | Java 技术栈统一 |
| 文档编辑工具 | Qt | QTextDocument 强大,性能优秀 |
集成能力
// JavaFX + Spring Boot 集成
@SpringBootApplication
public class App extends Application {
@Override
public void start(Stage stage) throws Exception {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/main.fxml"));
loader.setControllerFactory(springContext::getBean);
Parent root = loader.load();
stage.setScene(new Scene(root));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}// MAUI + 依赖注入
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// 依赖注入
builder.Services.AddSingleton<IDataService, DataService>();
builder.Services.AddTransient<MainViewModel>();
builder.Services.AddTransient<MainPage>();
return builder.Build();
}// Qt + CMake 第三方库集成
find_package(Poco REQUIRED COMPONENTS Net Util)
find_package(OpenCV REQUIRED)
target_link_libraries(MyApp PRIVATE
Poco::Net
Poco::Util
${OpenCV_LIBS}
)综合对比表格
| 对比维度 | JavaFX | .NET MAUI | Qt |
|---|---|---|---|
| 开发语言 | Java | C# | C++ / QML |
| 声明式 UI | FXML | XAML | QML |
| UI 引擎 | Prism (自绘) | 平台原生控件 | Qt Quick Scene Graph |
| 架构模式 | MVC / MVVM | MVVM | MVP / MVC / MVVM |
| 数据绑定 | Properties + Bindings | Binding + INotifyPropertyChanged | Property + Binding (QML) |
| 样式系统 | CSS 风格 | XAML Styles + CSS | QSS (Qt Style Sheets) |
| 图表支持 | 内置 Chart | 第三方 (Microcharts) | QtCharts 模块 |
| Web 视图 | WebView (WebKit) | WebView (平台原生) | QtWebEngine (Chromium) |
| 多线程 | Platform.runLater | MainThread.Invoke | signal/slot + QThread |
| 打包工具 | jlink + jpackage | dotent publish | CMake + windeployqt |
| 包体(最小) | ~50MB (含 JRE) | ~30MB (含 .NET runtime) | ~10MB (静态链接) |
| 启动时间 | 2-5s | 1-3s | <1s |
| 内存基准 | ~150MB | ~100MB | ~50MB |
| 许可证 | GPL + Classpath Exception / 商业 | MIT | LGPL / GPL / 商业 |
| 移动端支持 | 有限 (Gluon) | 原生支持 (Android/iOS) | 商业版支持 |
| 嵌入式支持 | 不支持 | 不支持 | 全面支持 |
| 学习周期 | 2-4 周 | 2-4 周 | 4-8 周 (C++ 路径) / 1-2 周 (QML 路径) |
| 主要公司 | Gluon / 传统 Java 企业 | 微软 / .NET 企业 | Qt Group / 汽车 / 工业 |
| 未来趋势 | 稳定维护 | 积极发展(.NET 生态驱动) | 持续更新(嵌入式 + QML) |
选型建议
选择 JavaFX 的场景:
- 企业内部系统,Java 技术栈统一
- 需要对 Swing 遗留系统进行现代化迁移
- 团队 Java 开发经验丰富
- 需要复杂的数据可视化图表
选择 .NET MAUI 的场景:
- 需要同时覆盖移动端和桌面端的跨平台应用
- 团队 C# 开发经验丰富
- 需要与 Microsoft 生态(Azure, Office 365)深度集成
- 新的跨平台项目,希望单代码库覆盖最多平台
选择 Qt 的场景:
- 性能敏感型应用(实时渲染、高频数据采集)
- 嵌入式系统、工业控制、汽车 IVI
- 需要最小化部署包体和内存占用
- 跨平台 C++ 应用,需要原生性能
- 需要美观流畅的现代 UI(QML 路径)