gtkmm库之动作系统详解

在 gtkmm 中,动作系统提供了一种将应用程序的功能(如"打开文件"、"复制"、"粘贴")集中定义,然后通过菜单、工具栏或按钮等多种界面元素统一调用的优雅方式。这种方式实现了界面布局与业务逻辑的解耦,让代码更加清晰和易于维护。

下面我们来详细拆解其中的核心组件:Gio::Action(动作)、Gio::ActionGroup(动作组)和 Gio::ActionMap(动作映射)。

概念解析:核心组件的角色

这三个组件协同工作,构成了 gtkmm 动作系统的基石。

  • Gio::Action (动作) :代表一个可被用户触发的命令,例如一个"粘贴"操作。它封装了动作的名称、是否可用(enabled)、是否带有状态(如布尔值表示"加粗"的开/关),以及激活时需要携带的参数类型。你可以把它理解为应用程序功能的抽象定义

  • Gio::ActionGroup (动作组) :顾名思义,它是一个包含多个 Gio::Action 的集合。这个接口定义了如何与组内的动作进行交互,例如通过 activate_action() 激活一个动作,或通过 change_action_state() 改变一个状态化动作的当前值。它是对外提供动作服务的公共接口

  • Gio::ActionMap (动作映射) :这是一个用于管理动作容器的接口,专注于如何添加、查找和移除动作。它的独特之处在于"映射"------可以将来自不同组的动作名称加上前缀(如"app."或"win.")以创建唯一的标识符,从而方便地在整个应用程序中引用它们。

在实践中,这三个概念紧密相关。Gio::ActionMap 通常由具体的类(如 Gio::SimpleActionGroup)实现,这个类本身也是一个 Gio::ActionGroup。因此,你创建的一个动作组对象,既可以用来管理动作(通过 ActionMap 接口),也可以被其他代码用来调用这些动作(通过 ActionGroup 接口)。

详细用法:从创建到调用

接下来,我们通过代码示例来看如何在 gtkmm 项目中使用它们。

1. 创建动作

创建动作最常用的方式是使用 Gio::SimpleAction。gtkmm 的 Gio::ActionMap 接口(例如在 Gtk::ApplicationWindowGio::SimpleActionGroup 中)提供了便捷的 add_action() 系列方法。

  • 无参数的动作:适用于"退出"这样的命令式操作。
cpp 复制代码
// 假设这是在继承自 Gtk::ApplicationWindow 的类中
add_action( "quit", sigc::mem_fun( *this, &MyWindow::on_action_quit ) );

这行代码创建了一个名为 "quit" 的动作,并将其与成员函数 on_action_quit 连接起来。

  • 带参数的动作:适用于需要接收数据的操作,例如通过一个链接地址来打开该链接。
cpp 复制代码
// 使用 add_action_with_parameter() 并指定参数类型
add_action_with_parameter( "open-link",
                           Glib::VARIANT_TYPE_STRING, // 参数类型为字符串
                           [this]( const Glib::VariantBase& parameter ) {
    // 从参数中提取字符串值
    Glib::ustring uri = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(parameter).get();
    // ... 执行打开链接的操作
});

这段代码展示了如何创建一个需要字符串参数的 "open-link" 动作,并在 Lambda 表达式中处理传入的 Glib::VariantBase 参数。

  • 带状态的动作:适用于有"开/关"或"多选一"状态的操作,例如切换粗体格式。
cpp 复制代码
// 创建一个布尔型状态的动作,初始状态为 false
auto pAction = Gio::SimpleAction::create_bool( "bold", false );
pAction->signal_change_state().connect( [this]( const Glib::VariantBase& state ) {
    bool new_state = Glib::VariantBase::cast_dynamic<Glib::Variant<bool>>(state).get();
    // 更新状态,例如设置文本为粗体
    set_text_bold( new_state );
    // 关键:更新动作的状态,使其与内部状态同步
    std::dynamic_pointer_cast<Gio::SimpleAction>(pAction)->set_state( state );
});
// 将创建好的动作添加到 ActionMap 中
add_action( pAction );

这里,我们连接到 signal_change_state() 信号,而不是 activate()。当界面(如菜单项)试图改变动作状态时,此信号会被触发。在回调中,我们执行实际的功能,并通过 set_state() 显式地更新动作本身的状态,这样所有关联的 UI 控件(如菜单项的复选框)都会自动更新。

2. 组织动作:ActionGroup 和 ActionMap

你可以直接将动作添加到支持 ActionMap 的窗口类(如 Gtk::ApplicationWindow)中,如上例所示。对于更复杂的应用,将动作分组到自定义的 Gio::SimpleActionGroup 中会更好。

cpp 复制代码
// 创建一个动作组
m_refActionGroup = Gio::SimpleActionGroup::create();

// 向组中添加动作 (通过 ActionMap 接口)
m_refActionGroup->add_action( "open", sigc::mem_fun( *this, &MyWindow::on_action_file_open ) );
m_refActionGroup->add_action( "quit", sigc::mem_fun( *this, &MyWindow::on_action_file_quit ) );

// 将整个动作组插入到窗口,并指定其前缀为 "example"
insert_action_group( "example", m_refActionGroup );

insert_action_group() 是关键步骤,它将你的动作组注册到窗口,并为组内所有动作添加了 "example." 这个前缀。

3. 连接 UI 与动作

动作的生命力在于与菜单、工具栏等 UI 元素的绑定。这通常通过 Gtk::BuilderGio::MenuModel 来实现。

首先,使用 XML 格式的 UI 字符串定义菜单布局,并通过 action 属性指定要关联的、带前缀的动作名称:

cpp 复制代码
  "<interface>"
  "  <menu id='menubar'>"
  "    <submenu>"
  "      <attribute name='label' translatable='yes'>_File</attribute>"
  "      <section>"
  "        <item>"
  "          <attribute name='label' translatable='yes'>_Open</attribute>"
  "          <attribute name='action'>example.open</attribute>"
  "        </item>"
  "        <item>"
  "          <attribute name='label' translatable='yes'>_Quit</attribute>"
  "          <attribute name='action'>example.quit</attribute>"
  "        </item>"
  "      </section>"
  "    </submenu>"
  "  </menu>"
  "</interface>";

然后,在 C++ 代码中加载此 UI 描述并创建菜单栏:

cpp 复制代码
auto refBuilder = Gtk::Builder::create();
refBuilder->add_from_string(ui_info);
auto gmenu = refBuilder->get_object<Gio::Menu>("menubar");
auto pMenuBar = Gtk::make_managed<Gtk::PopoverMenuBar>(gmenu);
// ... 将 pMenuBar 添加到窗口

当用户点击菜单中的"Open"项时,系统会自动找到并激活 "example.open" 动作。

4. 调用动作

除了通过 UI 触发,你也可以在代码中手动激活一个动作。这需要使用完整的、带前缀的动作名称和 activate_action() 方法:

cpp 复制代码
// 激活无参数动作
activate_action( "example.quit" );

// 激活带参数的动作
auto variant = Glib::Variant<Glib::ustring>::create( "https://www.gnome.org" );
activate_action( "win.open-link", variant );

activate_action 方法由 Gio::ActionGroup 接口提供。

实用技巧总结

  1. 善用便捷的 add_action 重载 :对于无参数、布尔状态、字符串状态等常见类型的动作,优先使用 add_action(), add_action_bool(), add_action_radio_string() 等便捷方法,它们能帮你省去手动创建 Gio::SimpleAction 的样板代码。

  2. 区分 activatechange_state :对于无状态的动作,连接到 activate 信号。对于有状态的动作(如开关、单选),必须 连接到 change_state 信号,并在回调中处理新状态,然后调用 set_state() 更新动作状态。切勿在 activate 中处理状态变化。

  3. 利用前缀实现作用域 :通过 insert_action_group() 为动作组添加前缀(如 "app.""win."),是实现动作作用域分离的标准模式。这可以清晰地表明一个动作是属于整个应用程序还是某个特定窗口。

  4. 为动作设置快捷键 :使用 Gtk::Application::set_accel_for_action() 可以非常方便地为动作绑定全局键盘快捷键。你需要传入完整的动作名称和加速器字符串(如 <Primary>c 代表 Ctrl+C)。

cpp 复制代码
app->set_accel_for_action( "example.copy", "<Primary>c" );
  1. 管理资源文件 :将菜单定义文件(.ui)编译到程序的二进制资源中(使用 Gio::Resourceglib-compile-resources),可以避免处理外部文件,使程序部署更简单。

  2. 从旧版 Gtk::Action 迁移 :如果你接触过 gtkmm 2.x 的代码,请注意 Gtk::Action 及相关类在 gtkmm 3.x 和 4.x 中已被标记为废弃,并最终被 Gio::Action 框架完全取代。新的基于 Gio 的动作系统更加灵活,并与 GNOME 平台的底层技术(如 D-Bus)更好地集成。

相关推荐
竹之却2 小时前
Ubuntu 系统安装 Ollama 教程
linux·运维·ubuntu·ollama
宵时待雨2 小时前
C++笔记归纳13:map & set
开发语言·数据结构·c++·笔记·算法
sdm0704274 小时前
yum和开发工具vim/gcc
linux·服务器·centos
仰泳的熊猫7 小时前
题目2570:蓝桥杯2020年第十一届省赛真题-成绩分析
数据结构·c++·算法·蓝桥杯
如意.75910 小时前
【Linux开发工具实战】Git、GDB与CGDB从入门到精通
linux·运维·git
Thera77710 小时前
C++ 高性能时间轮定时器:从单例设计到 Linux timerfd 深度优化
linux·开发语言·c++
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ12 小时前
Linux 查询某进程文件所在路径 命令
linux·运维·服务器
君义_noip12 小时前
信息学奥赛一本通 1952:【10NOIP普及组】三国游戏 | 洛谷 P1199 [NOIP 2010 普及组] 三国游戏
c++·信息学奥赛·csp-s
旖-旎12 小时前
二分查找(x的平方根)(4)
c++·算法·二分查找·力扣·双指针