【发布时间】:2020-03-23 08:24:02
【问题描述】:
我正在尝试找出如何使用 c++17 最优雅地解决这个问题(没有提升!)。我正在开发一个提供 UI 控件的库。控件应该能够对事件做出反应,并且用户(使用库)应该能够为这些事件添加他的处理程序。我想避免使用Command pattern,因为这对用户来说太复杂了——他只想定义一个函数并将其分配给一些事件和 UI 控件。下面是一些示例代码:
// My library
class Base; // Base class for all the UI controls
class ListBox : public Base {
// handlers reacting on clicking the item in listbox
std::vector<std::variant<std::function<void()>,
std::function<void(int)>>> click_handlers_;
// handlers reacting on changing the name of the item in listbox
std::vector<std::variant<std::function<void()>,
std::function<void(std::string, std::string)>>> edit_handlers_;
public:
void AddHandler(Event ev, const std::function< ? ? ? > handler);
};
// USER's code
int main() {
ListBox lb;
lb.AddHandler(Event::Click, [](int index) { DoSomethingWithItem(i); });
lb.AddHandler(Event::Edit, [](std::string old_name, std::string new_name)
{ DoSomethingWithItemNames(old_name, new_name); });
lb.AddHandler(Event::Edit, []() { DoSomeCleanup(); });
}
当Event::Edit(enum 类型)事件发生时,它会触发需要两个参数的DoSomethingWithItemNames()。在那之后,DoSomeCleanup() 完成了它的工作并处理了事件(至少从用户的角度来看)。
- 问题:对于每个 LisBox 事件,我必须定义一个单独的处理程序向量。
- 问题:
ListBox::AddHandler方法的第二个参数的类型应该是什么?
有没有办法统一这些处理程序,让用户只能使用一个函数AddHandler(),而不是像AddClickHandler()和AddEditHandler()这样的单独函数?当然,函数类型应该静态检查,任何类型不匹配都应该在编译时报告给用户。有没有标准的方法来处理整个概念?请记住,每个控件都有其事件,我的想法是,每个事件都可以分配一个不带参数的处理函数。谢谢。
编辑
经过考虑,又出现了一个问题。图书馆的设计看起来像this。事件由系统生成,然后调用预定义的框架方法,我需要从中调用用户的处理程序(例如,在 Qt 中,当在 QListWidget 内双击项目时会发出信号itemDoubleClicked(QListWidgetItem *item))。因此,实现类 (FrameWorkAListBox) 需要访问存储的处理程序(当前位于包装类 ListBox 中)。打算使用更多的框架,那么我应该将它们存储在哪里以及如何存储它们,这样它们就不会重复并且架构保持易于扩展?
编辑 2
经过几次尝试和更多研究,我最终得到了这个解决方案:
// first define some aliases
template<class... Args>
using Handler = std::function<void(Args...)>; // event handler
template<class... Args>
using VariableHandler = std::variant<Args...>;
template<class T>
using Event = std::list<T>; // list of handler variants
// this is for easier std::visit use, taken from
https://en.cppreference.com/w/cpp/utility/variant/visit
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...)->overloaded<Ts...>;
// ListBox.cpp - interface class, used by the user
void ListBox::AddDoubleClickHandler(
const VariableHandler<Handler<>, Handler<int>>& handler) {
// forwards the handler to the implementation class (pushes the
// handler into the ev_double_click_ vector since wrapper class cannot
// have any other private fields aside from pointer to the
// implementation (according to pimpl idiom)
}
// ListBoxWindowsImpl.h - native Windows class representing UI control
class ListBoxImpl : public SomeNativeWindowsListBoxClass {
public:
// double-click event handler can have 0 or 1int argument
Event<VariableHandler<Handler<>, Handler<int>>> ev_double_click_;
// this gets called automatically by the system, when the user
// double-clicks an item within the listbox
void OnItemDoubleClick(int index) {
for (const auto& h : ev_double_click_) {
std::visit(overloaded {
[](Handler<> arg) { arg(); },
[index](Handler<int> arg) { arg(index); },
}, h);
}
}
我想知道是否有一种方法可以添加新的处理程序,而无需向 ListBox 类添加新函数。如果添加双击处理程序和新处理程序(例如选择更改、单击...)可以由单个函数完成,例如 ListBox::AddHandler,那就太好了。最好的情况是在添加新处理程序时不需要更改此方法的主体,以便不需要重新编译库的接口(在本例中为 ListBox 类)。谢谢。
【问题讨论】:
-
您可以提供两个或更多
AddHandlers - 用于功能一的每种风格。但是,为了防止Event::Click与std::function<void(std::string, std::string)>结合使用,您需要一种不同的方法 - 两个enums(每个只有一个值)。 (我曾在 gtkmm 中看到过这个技巧,以区分具有其他相同参数的构造函数。)您可以自行决定您更喜欢什么:AddClickHandler(f)与AddHandler(ClickEvent, f)。恕我直言,这是个人品味的问题...... -
我已经更新了我的答案。希望这会有所帮助,祝你好运。 :)
-
你好。我再次更新了我的答案。第三次是他们所说的魅力。希望这一次,我能解决你的问题。 :)
标签: c++ events c++17 function-pointers variant