如何安全地将包含的对象(菜单项)与需要容器的API(菜单)同步?

时间:2012-12-02 04:41:27

标签: c++ oop design-patterns synchronization wrapper

为了我自己的喜悦,我试图想出一种更好的方法来使用Windows API菜单。虽然我确实找到this one,并且可能实际上按类别区分菜单项的类型,但我还是会这样:


描述

class MenuItem {...};

class MenuBar { //for the main menu of a window (file, help, etc)
    std::list<MenuItem> items; //and whatever else
};

class PopupMenu { //for dropdown (and possibly context) menus
    std::list<MenuItem> items; //and whatever else
};

MenuItem类具有check()disable()setText()等功能,以及setType()等更常见的功能(字符串,位图,分隔符)和setState(启用,检查,默认)。

在我看来,Menu类本身会提供appendStringItem()appendIteminsertBitmapItem()等功能,并提供对项目本身的访问权限。我也在讨论是否有一个Menu基类,虽然很感兴趣,但这不是问题的主题。


通缉用法

如果这只是C ++,我就不会有问题。但是,我的问题出现在将我的菜单项与Windows使用的菜单项同步,因为我班级中的更改不会自动更改真实的菜单项。为了更改菜单项,必须为其提供菜单,以及该菜单中的位置或项目的ID。这是有道理的,但从使用角度来看,为什么我不能这样做:

MenuBar menuBar; //pretend this is filled
menuBar.itemAt(2).setText("New text");

问题

嗯,问题是我希望这会改变实际菜单上的菜单项,但不会。我需要一些方法来了解拥有该项目的菜单,因为每个项目都有内部存储的ID。

我可以在MenuBar中的相应插入函数中执行此操作:

bool MenuBar::someInsertionFunction(unsigned index, MenuItem newItem) {
    newItem.setOwner(*this); //owner as a raw HMENU type, with *this converting
    items.emplace_back(index, newItem); //index checked, of course
}

完成后,我必须检查MenuItem中的每个setter以确保所有者是有效的,如果是,则使用API​​函数更新项目。同样,在getter中,我将调用API函数来获取当前状态,但前提是所有者有效。这允许用户创建自己的MenuItem列表并通过该列表初始化Menu。此方法还允许用户完全访问以修改内部项列表而不会产生任何后果,因为MenuItem类可以很好地保护自己。

但这与我发现的一个好概念相反:为什么包含的对象应该知道包含它们的内容?是否存在处理此问题的设计模式,或者我最好打破此问题“规则”是为了能够让菜单项控制自己(以及其他菜单项),而不是由菜单控制?

1 个答案:

答案 0 :(得分:1)

我想出了一个我非常喜欢的答案,实际上。它结合了能够让菜单项自行更改,同时仍然保留其他项目的一些保护。

首先,MenuItem存储一个改变自身的函数:

std::function<BOOL(UINT, LPMENUITEMINFO)> changeRealItem;

此功能基于Windows API SetMenuItemInfo,但缺少一些参数。那是因为我们要在Menu类中绑定它:

Menu() {
    //one menu item as an example
    item.changeRealItem = std::bind(
        NonWinapiFunction(SetMenuItemInfo), //explained below
        *this,                              //with *this converting to a HMENU
        std::placeholders::_1,              //first argument is the ID
        FALSE,                              //previous is an ID, not a position
        std::placeholders::_2               //second argument is pointer to info
    );
}

现在,在MenuItemClass中,我基本上可以做到这一点:

MENUITEMINFO info = *this; //passing *this as an HMENU causes address problems
changeRealItem(id(), &info);

作为概念证明,我做了一个MessageBox示例:

#include <functional>
#include <windows.h>

template<typename Ret, typename... Args>
std::function<Ret(Args...)> NonWinapiFunction(Ret(*WINAPI func)(Args...)) {
    return std::function<Ret(Args...)>(func);
}

struct MenuItem {
    MenuItem(std::function<int(const char *)> func) : message(func){}

    void changeSomething(const char *text) const {
        message(text);
    }

private:
    std::function<int(const char *)> message;
};

struct Menu {
    Menu() : item(std::bind(NonWinapiFunction(MessageBox), nullptr, std::placeholders::_1, "Title", MB_OK)){}
    MenuItem &getItem() {
        return item;
    }

private:
    MenuItem item;
};

int main() {
    Menu menu;
    menu.getItem().changeSomething("I can't change other menu items!");
}

最后一位是NonWinapiFunction。问题是您无法使用std::bindWINAPI)调用约定在函数上调用__stdcall。为了避免这种情况,使用可变参数模板从函数中提取返回类型和参数类型,并返回具有相同签名的std::function,但使用正确的调用约定,然后可以将其与{{一起使用1}}。

另一个问题是可以将任意ID传递给函数,并且需要传递winapi结构地址所需的额外行。我相信两者都可以通用的方式解决(只要存在从包装器到包装类型的转换运算符),但我还没有完全理解它。