在 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::ApplicationWindow 或 Gio::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::Builder 和 Gio::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 接口提供。
实用技巧总结
-
善用便捷的
add_action重载 :对于无参数、布尔状态、字符串状态等常见类型的动作,优先使用add_action(),add_action_bool(),add_action_radio_string()等便捷方法,它们能帮你省去手动创建Gio::SimpleAction的样板代码。 -
区分
activate和change_state:对于无状态的动作,连接到activate信号。对于有状态的动作(如开关、单选),必须 连接到change_state信号,并在回调中处理新状态,然后调用set_state()更新动作状态。切勿在activate中处理状态变化。 -
利用前缀实现作用域 :通过
insert_action_group()为动作组添加前缀(如"app."或"win."),是实现动作作用域分离的标准模式。这可以清晰地表明一个动作是属于整个应用程序还是某个特定窗口。 -
为动作设置快捷键 :使用
Gtk::Application::set_accel_for_action()可以非常方便地为动作绑定全局键盘快捷键。你需要传入完整的动作名称和加速器字符串(如<Primary>c代表 Ctrl+C)。
cpp
app->set_accel_for_action( "example.copy", "<Primary>c" );
-
管理资源文件 :将菜单定义文件(
.ui)编译到程序的二进制资源中(使用Gio::Resource和glib-compile-resources),可以避免处理外部文件,使程序部署更简单。 -
从旧版
Gtk::Action迁移 :如果你接触过 gtkmm 2.x 的代码,请注意Gtk::Action及相关类在 gtkmm 3.x 和 4.x 中已被标记为废弃,并最终被Gio::Action框架完全取代。新的基于Gio的动作系统更加灵活,并与 GNOME 平台的底层技术(如 D-Bus)更好地集成。