强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Vala 语言入门教程 / 08 - GTK 应用开发

第 8 章:GTK 应用开发

GTK 是 GNOME 生态的核心 GUI 框架。Vala 与 GTK 的结合堪称完美——简洁的语法加上原生的 GObject 支持。


8.1 GTK 4 架构概览

┌─────────────────────────────────────┐
│         libadwaita (自适应 UI)       │
├─────────────────────────────────────┤
│            GTK 4 (GUI 框架)          │
├─────────────────────────────────────┤
│     GIO / Pango / Cairo / GdkPixbuf │
├─────────────────────────────────────┤
│            GLib / GObject            │
└─────────────────────────────────────┘

GTK 4 vs GTK 3 的主要区别

特性GTK 3GTK 4
渲染GDK + Cairo渲染节点树 + GPU
事件处理GdkEventGtkEventController
布局GtkBox + GtkGridGtkBox + GtkGrid + GtkCenterBox
动画GtkRevealerGtkPropertyAnimation
样式CSS 2.1CSS 2.1+
平滑滚动有限原生支持
自适应布局需要 libhandylibadwaita

8.2 第一个 GTK 应用

8.2.1 最小的 GTK 4 应用

// hello_gtk.vala
using Gtk;

// 应用程序类
public class MyApp : Adw.Application {
    public MyApp () {
        Object (
            application_id: "com.example.HelloApp",
            flags: ApplicationFlags.FLAGS_NONE
        );
    }

    protected override void activate () {
        // 创建窗口
        var window = new Adw.ApplicationWindow (this) {
            title = "Hello Vala + GTK",
            default_width = 400,
            default_height = 300
        };

        // 创建标签
        var label = new Label ("你好,Vala + GTK 4!");

        // 设置窗口内容
        window.set_content (label);

        // 显示窗口
        window.present ();
    }
}

void main (string[] args) {
    var app = new MyApp ();
    app.run (args);
}

编译命令

valac \
    --pkg gtk4 \
    --pkg libadwaita-1 \
    -o hello_gtk \
    hello_gtk.vala

Meson 构建

project('hello_gtk', 'vala', 'c',
  version: '1.0.0',
  meson_version: '>= 0.62.0'
)

gtk4_dep = dependency('gtk4')
libadwaita_dep = dependency('libadwaita-1')

executable('hello_gtk',
  'src/hello_gtk.vala',
  dependencies: [gtk4_dep, libadwaita_dep],
  install: true
)

8.3 窗口和应用结构

8.3.1 Adw.Application 结构

using Gtk;
using Adw;

public class MyApplication : Application {
    private ApplicationWindow window;

    public MyApplication () {
        Object (
            application_id: "com.example.MyApp",
            flags: ApplicationFlags.FLAGS_NONE
        );
    }

    protected override void activate () {
        // 使用 Adw.Application 的样式管理
        var style_manager = StyleManager.get_default ();
        style_manager.color_scheme = ColorScheme.PREFER_LIGHT;

        // 创建主窗口
        window = new ApplicationWindow (this) {
            title = "我的应用",
            default_width = 800,
            default_height = 600
        };

        // 构建 UI
        build_ui ();

        // 显示窗口
        window.present ();
    }

    private void build_ui () {
        // 使用 HeaderBar
        var header = new HeaderBar ();

        // 创建工具栏按钮
        var menu_button = new MenuButton ();
        menu_button.icon_name = "open-menu-symbolic";
        menu_button.tooltip_text = "菜单";
        header.pack_end (menu_button);

        // 创建主内容区域
        var content = new Box (Orientation.VERTICAL, 0);
        content.append (header);

        var label = new Label ("欢迎使用!");
        label.vexpand = true;
        content.append (label);

        // 设置窗口内容
        var toolbar_view = new ToolbarView ();
        toolbar_view.add_top_bar (header);
        toolbar_view.set_content (label);

        window.set_content (toolbar_view);
    }
}

void main (string[] args) {
    var app = new MyApplication ();
    app.run (args);
}

8.3.2 多窗口管理

using Gtk;
using Adw;

public class MultiWindowApp : Application {
    private GLib.List<ApplicationWindow> windows =
        new GLib.List<ApplicationWindow> ();

    public MultiWindowApp () {
        Object (
            application_id: "com.example.MultiWindow",
            flags: ApplicationFlags.FLAGS_NONE
        );
    }

    protected override void activate () {
        create_window ();
    }

    private ApplicationWindow create_window () {
        var window = new ApplicationWindow (this) {
            title = "窗口 %u".printf (windows.length () + 1),
            default_width = 400,
            default_height = 300
        };

        // 新建窗口按钮
        var button = new Button.with_label ("新建窗口");
        button.clicked.connect (() => {
            create_window ();
        });

        var box = new Box (Orientation.VERTICAL, 12);
        box.valign = Align.CENTER;
        box.halign = Align.CENTER;
        box.append (button);

        var toolbar_view = new ToolbarView ();
        var header = new HeaderBar ();
        toolbar_view.add_top_bar (header);
        toolbar_view.set_content (box);

        window.set_content (toolbar_view);
        window.present ();

        windows.append (window);
        window.destroy.connect (() => {
            windows.remove (window);
            if (windows.length () == 0) {
                quit ();
            }
        });

        return window;
    }
}

void main (string[] args) {
    var app = new MultiWindowApp ();
    app.run (args);
}

8.4 常用控件

8.4.1 文本和输入

using Gtk;
using Adw;

public class InputDemo : Adw.Application {
    public InputDemo () {
        Object (application_id: "com.example.InputDemo",
                flags: ApplicationFlags.FLAGS_NONE);
    }

    protected override void activate () {
        var window = new Adw.ApplicationWindow (this) {
            title = "输入控件演示",
            default_width = 500,
            default_height = 400
        };

        var box = new Box (Orientation.VERTICAL, 12) {
            margin_top = 24,
            margin_bottom = 24,
            margin_start = 24,
            margin_end = 24
        };

        // 标签
        var title = new Label ("用户信息");
        title.add_css_class ("title-1");
        box.append (title);

        // 文本输入
        var name_entry = new Entry () {
            placeholder_text = "请输入姓名"
        };
        box.append (create_row ("姓名", name_entry));

        // 密码输入
        var password_entry = new PasswordEntry () {
            placeholder_text = "请输入密码"
        };
        box.append (create_row ("密码", password_entry));

        // 数字输入
        var age_spin = new SpinButton.with_range (0, 150, 1) {
            value = 25
        };
        box.append (create_row ("年龄", age_spin));

        // 搜索框
        var search = new SearchEntry () {
            placeholder_text = "搜索..."
        };
        search.search_changed.connect (() => {
            print ("搜索: %s\n", search.text);
        });
        box.append (create_row ("搜索", search));

        // 多行文本
        var text_view = new TextView ();
        text_view.set_size_request (-1, 100);
        var scrolled = new ScrolledWindow () {
            child = text_view,
            vexpand = true
        };
        box.append (scrolled);

        // 按钮
        var button = new Button.with_label ("提交");
        button.add_css_class ("suggested-action");
        button.clicked.connect (() => {
            var buffer = text_view.buffer;
            string text;
            buffer.get_text (out text, null, false);
            print ("姓名: %s, 年龄: %.0f\n", name_entry.text, age_spin.value);
            print ("备注: %s\n", text);
        });
        box.append (button);

        window.set_content (box);
        window.present ();
    }

    private Adw.ActionRow create_row (string title, Widget widget) {
        var row = new Adw.ActionRow ();
        row.title = title;
        row.add_suffix (widget);
        return row;
    }
}

void main (string[] args) {
    new InputDemo ().run (args);
}

8.4.2 按钮和选择

using Gtk;
using Adw;

public class ButtonDemo : Adw.Application {
    public ButtonDemo () {
        Object (application_id: "com.example.ButtonDemo",
                flags: ApplicationFlags.FLAGS_NONE);
    }

    protected override void activate () {
        var window = new Adw.ApplicationWindow (this) {
            title = "按钮和选择",
            default_width = 500,
            default_height = 600
        };

        var box = new Box (Orientation.VERTICAL, 12) {
            margin_top = 24,
            margin_bottom = 24,
            margin_start = 24,
            margin_end = 24
        };

        // 普通按钮
        var normal_btn = new Button.with_label ("普通按钮");
        normal_btn.clicked.connect (() => {
            print ("普通按钮被点击\n");
        });
        box.append (normal_btn);

        // 图标按钮
        var icon_btn = new Button.from_icon_name ("document-new") {
            tooltip_text = "新建"
        };
        box.append (icon_btn);

        // 开关
        var toggle = new Switch () {
            active = true,
            halign = Align.START
        };
        toggle.state_set.connect ((state) => {
            print ("开关: %s\n", state ? "开" : "关");
            return false;
        });
        box.append (create_row ("通知", toggle));

        // 复选框
        var check = new CheckButton.with_label ("同意条款");
        check.toggled.connect (() => {
            print ("复选框: %s\n", check.active ? "选中" : "未选中");
        });
        box.append (check);

        // 单选按钮组
        var radio1 = new CheckButton.with_label ("选项 A");
        var radio2 = new CheckButton.with_label ("选项 B");
        radio2.set_group (radio1);
        var radio3 = new CheckButton.with_label ("选项 C");
        radio3.set_group (radio1);
        radio1.active = true;

        var radio_box = new Box (Orientation.VERTICAL, 6);
        radio_box.append (radio1);
        radio_box.append (radio2);
        radio_box.append (radio3);
        box.append (radio_box);

        // 下拉菜单
        var dropdown = new DropDown.from_strings (
            {"选项一", "选项二", "选项三", "选项四"}
        );
        dropdown.notify["selected"].connect (() => {
            print ("选择: %u\n", dropdown.selected);
        });
        box.append (create_row ("选择", dropdown));

        // 滑块
        var scale = new Scale.with_range (Orientation.HORIZONTAL, 0, 100, 1) {
            value = 50,
            hexpand = true
        };
        scale.value_changed.connect (() => {
            print ("滑块值: %.0f\n", scale.get_value ());
        });
        box.append (create_row ("音量", scale));

        // 进度条
        var progress = new ProgressBar () {
            fraction = 0.65,
            show_text = true,
            text = "65%"
        };
        box.append (progress);

        window.set_content (box);
        window.present ();
    }

    private Adw.ActionRow create_row (string title, Widget widget) {
        var row = new Adw.ActionRow ();
        row.title = title;
        row.add_suffix (widget);
        return row;
    }
}

void main (string[] args) {
    new ButtonDemo ().run (args);
}

8.4.3 列表和树视图

using Gtk;
using Adw;

public class ListDemo : Adw.Application {
    public ListDemo () {
        Object (application_id: "com.example.ListDemo",
                flags: ApplicationFlags.FLAGS_NONE);
    }

    protected override void activate () {
        var window = new Adw.ApplicationWindow (this) {
            title = "列表演示",
            default_width = 400,
            default_height = 500
        };

        // 使用 ListBox(GTK 4 推荐)
        var listbox = new ListBox ();
        listbox.selection_mode = SelectionMode.SINGLE;

        // 添加列表项
        string[] items = {
            "文件管理器", "终端", "文本编辑器",
            "浏览器", "音乐播放器", "视频播放器"
        };

        foreach (var item in items) {
            var row = new Adw.ActionRow ();
            row.title = item;
            row.subtitle = "应用程序";
            row.add_suffix (new Image.from_icon_name ("go-next-symbolic"));
            listbox.append (row);
        }

        // 选择事件
        listbox.row_selected.connect ((row) => {
            if (row != null) {
                var action_row = (Adw.ActionRow) row;
                print ("选中: %s\n", action_row.title);
            }
        });

        // 使用 ColumnView(更高级的列表)
        var model = new GLib.ListStore (typeof (string));
        model.append ("项目 1");
        model.append ("项目 2");
        model.append ("项目 3");

        var scrolled = new ScrolledWindow () {
            child = listbox,
            vexpand = true
        };

        window.set_content (scrolled);
        window.present ();
    }
}

void main (string[] args) {
    new ListDemo ().run (args);
}

8.5 布局系统

8.5.1 Box 布局

using Gtk;
using Adw;

public class LayoutDemo : Adw.Application {
    public LayoutDemo () {
        Object (application_id: "com.example.LayoutDemo",
                flags: ApplicationFlags.FLAGS_NONE);
    }

    protected override void activate () {
        var window = new Adw.ApplicationWindow (this) {
            title = "布局演示",
            default_width = 600,
            default_height = 400
        };

        // 垂直布局
        var vbox = new Box (Orientation.VERTICAL, 12) {
            margin_top = 24,
            margin_bottom = 24,
            margin_start = 24,
            margin_end = 24
        };

        // 水平布局
        var hbox = new Box (Orientation.HORIZONTAL, 12);

        var btn1 = new Button.with_label ("按钮 1");
        btn1.hexpand = true;
        var btn2 = new Button.with_label ("按钮 2");
        btn2.hexpand = true;
        var btn3 = new Button.with_label ("按钮 3");
        btn3.hexpand = true;

        hbox.append (btn1);
        hbox.append (btn2);
        hbox.append (btn3);
        vbox.append (hbox);

        // Grid 布局
        var grid = new Grid () {
            row_spacing = 12,
            column_spacing = 12,
            hexpand = true
        };

        grid.attach (new Label ("用户名:"), 0, 0);
        grid.attach (new Entry () { hexpand = true }, 1, 0);
        grid.attach (new Label ("密码:"), 0, 1);
        grid.attach (new PasswordEntry () { hexpand = true }, 1, 1);

        var login_btn = new Button.with_label ("登录");
        login_btn.add_css_class ("suggested-action");
        grid.attach (login_btn, 0, 2, 2, 1);

        vbox.append (grid);

        // CenterBox(三栏布局)
        var center = new CenterBox ();
        center.set_start_widget (new Label ("左"));
        center.set_center_widget (new Label ("中"));
        center.set_end_widget (new Label ("右"));
        vbox.append (center);

        // 带分割线的布局
        var paned = new Paned (Orientation.HORIZONTAL) {
            start_child = new Label ("左侧内容"),
            end_child = new Label ("右侧内容"),
            position = 200,
            vexpand = true
        };
        vbox.append (paned);

        window.set_content (vbox);
        window.present ();
    }
}

void main (string[] args) {
    new LayoutDemo ().run (args);
}

8.5.2 响应式布局(libadwaita)

using Gtk;
using Adw;

public class AdaptiveDemo : Adw.Application {
    public AdaptiveDemo () {
        Object (application_id: "com.example.AdaptiveDemo",
                flags: ApplicationFlags.FLAGS_NONE);
    }

    protected override void activate () {
        var window = new Adw.ApplicationWindow (this) {
            title = "自适应布局",
            default_width = 400,
            default_height = 600
        };

        // 使用 Adw.NavigationSplitView(自适应导航)
        var sidebar = new Adw.NavigationPage (
            create_sidebar (),
            "侧边栏"
        );

        var content = new Adw.NavigationPage (
            create_content (),
            "内容"
        );

        var split_view = new Adw.NavigationSplitView () {
            sidebar = sidebar,
            content = content,
            min_sidebar_width = 200,
            max_sidebar_width = 300
        };

        window.set_content (split_view);
        window.present ();
    }

    private Widget create_sidebar () {
        var listbox = new ListBox ();
        for (int i = 1; i <= 10; i++) {
            var row = new Adw.ActionRow ();
            row.title = "项目 %d".printf (i);
            listbox.append (row);
        }

        var scrolled = new ScrolledWindow () {
            child = listbox,
            vexpand = true
        };

        var box = new Box (Orientation.VERTICAL, 0);
        var header = new Adw.HeaderBar ();
        box.append (header);
        box.append (scrolled);

        return box;
    }

    private Widget create_content () {
        var label = new Label ("选择左侧的项目") {
            vexpand = true
        };

        var box = new Box (Orientation.VERTICAL, 0);
        var header = new Adw.HeaderBar ();
        box.append (header);
        box.append (label);

        return box;
    }
}

void main (string[] args) {
    new AdaptiveDemo ().run (args);
}

8.6 对话框

8.6.1 消息对话框

using Gtk;
using Adw;

public class DialogDemo : Adw.Application {
    private Adw.ApplicationWindow window;

    public DialogDemo () {
        Object (application_id: "com.example.DialogDemo",
                flags: ApplicationFlags.FLAGS_NONE);
    }

    protected override void activate () {
        window = new Adw.ApplicationWindow (this) {
            title = "对话框演示",
            default_width = 400,
            default_height = 300
        };

        var box = new Box (Orientation.VERTICAL, 12) {
            margin_top = 24,
            margin_bottom = 24,
            margin_start = 24,
            margin_end = 24,
            valign = Align.CENTER,
            halign = Align.CENTER
        };

        // 消息对话框
        var info_btn = new Button.with_label ("信息对话框");
        info_btn.clicked.connect (show_info_dialog);
        box.append (info_btn);

        // 确认对话框
        var confirm_btn = new Button.with_label ("确认对话框");
        confirm_btn.clicked.connect (show_confirm_dialog);
        box.append (confirm_btn);

        // 输入对话框
        var input_btn = new Button.with_label ("输入对话框");
        input_btn.clicked.connect (show_input_dialog);
        box.append (input_btn);

        // 文件选择器
        var file_btn = new Button.with_label ("文件选择器");
        file_btn.clicked.connect (show_file_dialog);
        box.append (file_btn);

        window.set_content (box);
        window.present ();
    }

    private void show_info_dialog () {
        var dialog = new Adw.AlertDialog ("提示", "操作已完成!");
        dialog.add_response ("ok", "确定");
        dialog.default_response = "ok";
        dialog.present (window);
    }

    private void show_confirm_dialog () {
        var dialog = new Adw.AlertDialog ("确认", "确定要删除吗?");
        dialog.add_response ("cancel", "取消");
        dialog.add_response ("delete", "删除");
        dialog.set_response_appearance ("delete", Adw.ResponseAppearance.DESTRUCTIVE);
        dialog.default_response = "cancel";
        dialog.close_response = "cancel";

        dialog.response.connect ((response) => {
            if (response == "delete") {
                print ("已删除\n");
            }
        });

        dialog.present (window);
    }

    private void show_input_dialog () {
        var entry = new Entry () {
            placeholder_text = "请输入内容"
        };

        var dialog = new Adw.AlertDialog ("输入", null);
        dialog.set_extra_child (entry);
        dialog.add_response ("cancel", "取消");
        dialog.add_response ("ok", "确定");
        dialog.default_response = "ok";

        dialog.response.connect ((response) => {
            if (response == "ok") {
                print ("输入内容: %s\n", entry.text);
            }
        });

        dialog.present (window);
    }

    private async void show_file_dialog () {
        var dialog = new FileDialog ();
        dialog.title = "选择文件";

        try {
            var file = yield dialog.open (window, null);
            if (file != null) {
                print ("选择的文件: %s\n", file.get_path ());
            }
        } catch (GLib.Error e) {
            printerr ("错误: %s\n", e.message);
        }
    }
}

void main (string[] args) {
    new DialogDemo ().run (args);
}

8.7 GResource

8.7.1 资源文件

GResource 将文件(图片、UI、CSS 等)嵌入到二进制文件中。

目录结构

data/
├── resources.gresource.xml
├── ui/
│   └── window.ui
├── icons/
│   └── hicolor/
│       └── scalable/
│           └── apps/
│               └── com.example.MyApp.svg
└── style.css

resources.gresource.xml

<?xml version="1.0" encoding="UTF-8"?>
<gresources>
  <gresource prefix="/com/example/MyApp">
    <file preprocess="xml-stripblanks">ui/window.ui</file>
    <file>style.css</file>
    <file preprocess="xml-stripblanks">icons/hicolor/scalable/apps/com.example.MyApp.svg</file>
  </gresource>
</gresources>

meson.build(资源编译):

gnome = import('gnome')

resources = gnome.compile_resources(
  'resources',
  'data/resources.gresource.xml',
  source_dir: 'data',
  c_name: 'myapp'
)

executable('myapp',
  'src/main.vala',
  resources,
  dependencies: [gtk4_dep, libadwaita_dep],
  install: true
)

8.7.2 在代码中使用资源

// 加载 CSS
var css_provider = new CssProvider ();
css_provider.load_from_resource ("/com/example/MyApp/style.css");
StyleContext.add_provider_for_display (
    Gdk.Display.get_default (),
    css_provider,
    Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
);

// 加载图标
var icon = new GLib.ThemedIcon ("com.example.MyApp");

// 加载 UI 文件
var builder = new Gtk.Builder ();
builder.add_from_resource ("/com/example/MyApp/ui/window.ui", null);

8.8 UI 文件(Builder)

8.8.1 使用 Gtk.Builder 加载 UI

window.ui

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <requires lib="gtk" version="4.0"/>
  <requires lib="libadwaita" version="1.0"/>

  <object class="AdwApplicationWindow" id="main_window">
    <property name="title">My App</property>
    <property name="default-width">800</property>
    <property name="default-height">600</property>

    <child>
      <object class="GtkBox" id="main_box">
        <property name="orientation">vertical</property>

        <child>
          <object class="AdwHeaderBar" id="header"/>
        </child>

        <child>
          <object class="GtkLabel" id="welcome_label">
            <property name="label">欢迎!</property>
            <property name="vexpand">true</property>
            <property name="css-classes">title-1</property>
          </object>
        </child>

        <child>
          <object class="GtkButton" id="action_button">
            <property name="label">点击我</property>
            <property name="css-classes">suggested-action</property>
            <signal name="clicked" handler="on_action_clicked"/>
          </object>
        </child>
      </object>
    </child>
  </object>
</interface>

main.vala

using Gtk;
using Adw;

[GtkTemplate (ui = "/com/example/MyApp/ui/window.ui")]
public class MyWindow : Adw.ApplicationWindow {
    [GtkChild]
    private unowned Label welcome_label;

    [GtkChild]
    private unowned Button action_button;

    public MyWindow (Adw.Application app) {
        Object (application: app);
    }

    [GtkCallback]
    private void on_action_clicked () {
        welcome_label.label = "按钮已被点击!";
    }
}

public class MyApp : Adw.Application {
    public MyApp () {
        Object (application_id: "com.example.MyApp",
                flags: ApplicationFlags.FLAGS_NONE);
    }

    protected override void activate () {
        var window = new MyWindow (this);
        window.present ();
    }
}

void main (string[] args) {
    new MyApp ().run (args);
}

8.9 业务场景:联系人管理应用

using Gtk;
using Adw;

// 联系人数据模型
public class Contact : Object {
    public string name { get; set; }
    public string phone { get; set; }
    public string email { get; set; }

    public Contact (string name, string phone, string email) {
        Object (name: name, phone: phone, email: email);
    }
}

// 主应用
public class ContactApp : Adw.Application {
    private GLib.ListStore contacts;
    private ListBox listbox;

    public ContactApp () {
        Object (application_id: "com.example.ContactApp",
                flags: ApplicationFlags.FLAGS_NONE);
    }

    protected override void activate () {
        contacts = new GLib.ListStore (typeof (Contact));

        // 添加示例数据
        contacts.append (new Contact ("张三", "138-0000-0001", "[email protected]"));
        contacts.append (new Contact ("李四", "139-0000-0002", "[email protected]"));
        contacts.append (new Contact ("王五", "137-0000-0003", "[email protected]"));

        var window = new Adw.ApplicationWindow (this) {
            title = "联系人",
            default_width = 400,
            default_height = 600
        };

        // 构建界面
        var toolbar_view = new ToolbarView ();

        var header = new Adw.HeaderBar ();
        var add_button = new Button.from_icon_name ("list-add-symbolic");
        add_button.tooltip_text = "添加联系人";
        add_button.clicked.connect (() => {
            show_add_dialog (window);
        });
        header.pack_end (add_button);
        toolbar_view.add_top_bar (header);

        // 列表
        listbox = new ListBox ();
        listbox.selection_mode = SelectionMode.SINGLE;
        listbox.add_css_class ("boxed-list");

        // 绑定数据
        bind_list ();

        var scrolled = new ScrolledWindow () {
            child = listbox,
            vexpand = true,
            hexpand = true
        };

        toolbar_view.set_content (scrolled);
        window.set_content (toolbar_view);
        window.present ();
    }

    private void bind_list () {
        // 手动绑定列表
        for (uint i = 0; i < contacts.get_n_items (); i++) {
            var contact = (Contact) contacts.get_item (i);
            listbox.append (create_contact_row (contact));
        }
    }

    private Widget create_contact_row (Contact contact) {
        var row = new Adw.ActionRow ();
        row.title = contact.name;
        row.subtitle = "%s | %s".printf (contact.phone, contact.email);
        row.add_prefix (new Image.from_icon_name ("avatar-default-symbolic"));

        var delete_btn = new Button.from_icon_name ("user-trash-symbolic") {
            css_classes = {"flat", "circular"},
            tooltip_text = "删除"
        };
        delete_btn.clicked.connect (() => {
            print ("删除: %s\n", contact.name);
        });
        row.add_suffix (delete_btn);

        return row;
    }

    private void show_add_dialog (Adw.ApplicationWindow parent) {
        var name_entry = new Entry () { placeholder_text = "姓名" };
        var phone_entry = new Entry () { placeholder_text = "电话" };
        var email_entry = new Entry () { placeholder_text = "邮箱" };

        var box = new Box (Orientation.VERTICAL, 12);
        box.append (name_entry);
        box.append (phone_entry);
        box.append (email_entry);

        var dialog = new Adw.AlertDialog ("添加联系人", null);
        dialog.set_extra_child (box);
        dialog.add_response ("cancel", "取消");
        dialog.add_response ("add", "添加");
        dialog.default_response = "add";

        dialog.response.connect ((response) => {
            if (response == "add") {
                var contact = new Contact (
                    name_entry.text,
                    phone_entry.text,
                    email_entry.text
                );
                contacts.append (contact);
                listbox.append (create_contact_row (contact));
            }
        });

        dialog.present (parent);
    }
}

void main (string[] args) {
    new ContactApp ().run (args);
}

8.10 注意事项

⚠️ GTK 开发常见陷阱

  1. 使用 GTK 4 而非 GTK 3:新项目应使用 GTK 4
  2. libadwaita 是推荐的:它提供了 GNOME 风格的自适应控件
  3. UI 线程:GTK 不是线程安全的,所有 UI 操作必须在主线程
  4. 长耗时操作:使用 async 避免阻塞 UI
  5. 资源编译:使用 Meson 的 gnome.compile_resources() 编译资源
  6. CSS 样式:GTK 4 使用 CSS 进行样式化

8.11 扩展阅读

资源链接
GTK 4 文档https://docs.gtk.org/gtk4/
libadwaita 文档https://gnome.pages.gitlab.gnome.org/libadwaita/
GTK 4 Widget 工厂https://apps.gnome.org/Workbench/
GNOME 开发者指南https://developer.gnome.org/
GTK 4 示例https://gitlab.gnome.org/GNOME/gtk/-/tree/main/examples

8.12 总结

要点说明
框架GTK 4 + libadwaita
应用类Adw.Application
窗口类Adw.ApplicationWindow
布局BoxGridCenterBoxPaned
对话框Adw.AlertDialogFileDialog
资源GResource + gnome.compile_resources()
UI 文件Gtk.Builder + .ui XML

下一章我们将学习 POSIX 绑定和系统编程。→ 第 9 章:POSIX 绑定