GTKMM C++ 的更简单的 MRU
有时,你需要一把锤子来完成工作,而不是一枚火箭筒。
我猜我是一个虐待狂,或者像逆风飞翔的小鸟;我真的很喜欢开发桌面应用程序。现在一切都关于手机应用和 Web 应用,但我仍然很喜欢使用我的桌面并为其编写应用程序。
人们反对将 Linux 作为其桌面操作系统最常听到的抱怨是缺乏应用程序。嗯,这不会改变,除非人们开始编写这些应用程序。
就桌面应用程序设计而言,一切都应该尽可能简单,或者这就是我的理念。应用程序设计的所有组件都应该遵循这个简单的规则,只要说得通,而且我认为没有任何理由不将这个原则纳入朴实的 RUF,或者最近使用文件列表,也称为 MRU,或者最近使用,等等...
MRU(从现在开始)是一种传统,应该在任何鼠标驱动的 GUI 上都可以使用,并且应该易于在任何工具包或框架中实现。当我开始研究 GTKMM 中实现的 MRU 对象时,我感到多么惊讶;它几乎是我见过的最复杂的类集合。而且,为了只显示与您的应用程序相关的最近文件,您需要采用某种过滤器,然后我会读到网上关于这些对象的各种抱怨,在那之后,我停止了担心,而是编写了自己的类。
我非常尊重 Murry Cumming 和 GTKMM 团队以及整个 GTK 项目,维护一套统一的 API 并使其在像 GNU/Linux/Gnome 这样的动态平台上正常工作是一项巨大的努力,我当然知道。我也意识到,开发人员或组织实现一套功能集通常都有很多潜在的原因。但有时,你只是希望事物按照你想要的方式工作。
当我更深入地了解 GTKMM 的 `RecentChooser` 类(有基类 `Widget`,一个对话框对象,一个动作对象,以及一个菜单对象,然后是您需要使用的过滤器,等等)时,我只是耸了耸肩,告诉自己“我不做这个”。我理解所有变体,显然动作对象是为了让你可以在编辑菜单中添加一个历史选项,随便什么。我只想让用户点击一个包含 MRU 的动态菜单。
所以,撇开那些不谈,我将为您带来一种使用 STL 和 GTKMM 自有菜单 API 的更简单的方法。
Using the Code
我目前为 GNOME 桌面开发的应用程序是一个经典的 Model-View-Controller 实现,带有一个菜单栏,当然,在“文件”菜单项下是我的 MRU。
最简单的 MRU 就是一个 FIFO,而 `std::deque` 非常适合这项工作。在我的应用程序的数据类(实际上是 `struct` 的集合)中,我引用了一个 `std::deque` 对象。
我开始时添加了一些文件菜单项,文件 1..4,并将它们绑定到一个虚拟函数。我知道以后可以更改菜单标签并将它们绑定到实际函数(从我的应用程序的 `Glib::ustring ui_info`);
"<Menuitem action=<'FileOpen'/>"
"<separator/>"
"<menuitem action='file1'/>"
"<menuitem action='file2'/>"
"<menuitem action='file3'/>"
"<menuitem action='file4'/>"
(并且来自我的应用程序的 `Gtk::ActionGroup` 对象)
m_refActionGroup->add(Gtk::Action::create("file1", "file1",
"Reopen this file"), sigc::mem_fun(*this, &ExampleWindow::on_dummy));
m_refActionGroup->add(Gtk::Action::create("file2", "file1",
"Reopen this file"), sigc::mem_fun(*this, &ExampleWindow::on_dummy));
m_refActionGroup->add(Gtk::Action::create("file3", "file1",
"Reopen this file"), sigc::mem_fun(*this, &ExampleWindow::on_dummy));
m_refActionGroup->add(Gtk::Action::create("file4", "file1",
"Reopen this file"), sigc::mem_fun(*this, &ExampleWindow::on_dummy));
“`on_dummy`”方法只是一个空方法,我们需要它,因为 `ActionGroup` 模板要求它被填充,我们稍后会用实际方法来填充它。这些菜单项必须在我们应用程序初始化时隐藏,或者用上一个会话生成的列表来填充;
Gtk::Widget* file1 = m_refUIManager->get_widget("/ui/MenuBar/FileMenu/file1");
Gtk::Widget* file2 = m_refUIManager->get_widget("/ui/MenuBar/FileMenu/file2");
Gtk::Widget* file3 = m_refUIManager->get_widget("/ui/MenuBar/FileMenu/file3");
Gtk::Widget* file4 = m_refUIManager->get_widget("/ui/MenuBar/FileMenu/file4");
file1->hide();
file2->hide();
file3->hide();
file4->hide();
我的文件打开菜单项在激活时,除了打开文件,还会获取来自文件选择器的路径,并将其发送到一个方法,该方法会检查 deque 的当前大小,然后将文件插入到 deque 对象中。
在一个头文件中,我们有这些声明(关于信号数组 '`mru_sig`' 稍后详述)
std::deque<Glib::ustring> mru; // our deque object
sigc::connection mru_sig[4]; // dynamic menu signals
然后在实现文件中,在我们的“mru 管理器”方法中,`app` 只是一个指向“应用程序结构”的指针,这是一个包含 deque 对象等内容的结构。
// If the deque is more than four, we need to pop one file off it
if(app->mru.size() >= 4)
app->mru.pop_back();
// then add the new file
app->mru.push_front(str);
// may not be nessessary
app->mru.resize(4);
相当简单的东西。现在,每次打开一个文件,它都会被放入我们的 deque 对象中,并且每次放置新文件时都会通过循环轮换到列表的底部。在这种情况下,我将最近文件的数量保持为 `4`,但如果有人愿意,可以通过添加一个整数类成员并使用它而不是上面的“`4`”常数来调整该数字或使其用户可配置。
然后就是菜单信号的重新分配,在方法代码的早期,我通过一个简单的指针数组指向一些空的 `Gtk::Widgets`。
Glib::RefPtr<Gtk::UIManager> m_refUIManager; // Typical GTKMM stuff
Gtk::Widget* file[4];
file[0] = m_refUIManager->get_widget("/ui/MenuBar/FileMenu/file1");
file[1] = m_refUIManager->get_widget("/ui/MenuBar/FileMenu/file2");
file[2] = m_refUIManager->get_widget("/ui/MenuBar/FileMenu/file3");
file[3] = m_refUIManager->get_widget("/ui/MenuBar/FileMenu/file4");
路径是指 Gtk XML GUI 中的 UI 文件菜单,如果您熟悉使用 GTKMM 进行 Gtk 桌面编程,您应该知道它是如何工作的。我们需要将它们作为动态菜单的引用,我们将连接激活信号到这些菜单项。说到这里,下面是这些信号如何连接到我们的菜单项:
int n = 0;
for(deque<Glib::ustring>::iterator it =
app->mru.begin(); it < app->mru.end(); ++it) {
const Glib::ustring& label =
(*it).substr((*it).find_last_of("//") + 1, (*it).length());
dynamic_cast<Gtk::MenuItem*>(file[n])->set_label(label.c_str());
app->mru_sig[n].disconnect();
app->mru_sig[n] = dynamic_cast<Gtk::MenuItem*>(file[n])->signal_activate().
connect(sigc::bind(sigc::mem_fun(*this, &ExampleWindow::on_mru), label));
if(dynamic_cast<Gtk::MenuItem*>(file[n])->get_label().length() > 0)
file[n++]->show();
}
我们遍历我们存储的 4 个文件路径列表,取出最后一部分作为我们在菜单中显示的名称,然后我们对该项执行通用的信号断开连接。如果我们不这样做,信号将堆积在该项上,并且我们的“打开文件”方法将收到多个文件路径。然后我们连接一个新信号,该信号与我们想要菜单项打开的文件数据路径绑定。
信号方法本身非常简单。
on_mru(Glib::ustring& label)
{
deque<Glib::ustring>::iterator it = _app->mru.begin();
for(; it < _app->mru.end(); ++it) {
const Glib::ustring& text =
(*it).substr((*it).find_last_of("//") + 1, (*it).length());
if(text.find(label) != string::npos)
// this is the file the user clicked on in the MRU
cout << (*it).c_str() << endl;
}
}
在菜单标签上绑定的文本在 deque 对象中被搜索,如果我们找到匹配项,我们就获得了最近处理文件的完整路径。
关注点
我不确定动态菜单初始化区域中的虚拟方法是否完全必要,我需要对代码进行一些尝试,如果可能的话使用实际的“`on_mru`”方法,我认为我使用了这种技术是因为我没有要绑定到信号的数据,但一个空的 `string` 可能也能起到同样的作用。
“`on_mru`”方法只是从 deque 对象中获取文件的完整路径并在控制台中显示它,显然您需要将其传递给应用程序的打开文件方法。
该示例需要 libgtkmm 2.4,并使用以下方式构建:
g++ -Wall -std=c++11 examplewindow.cc main.cc -o menu `pkg-config --cflags --libs gtkmm-2.4`
历史
- 2014/8/15:第一个版本