【问题标题】:Iterate Over Struct; Easily Display Struct Fields And Values In a RichEdit Box迭代结构;在 RichEdit 框中轻松显示结构字段和值
【发布时间】:2010-12-25 02:21:01
【问题描述】:

有没有更简单的方法在RichEdit 控件中显示struct 字段及其对应值?

这就是我现在正在做的事情:

AnsiString s;

s = IntToStr(wfc.fontColor);
RichEdit1->Lines->Append(s);

等等……

有没有比必须单独调用每个人更简单的方法?我想读取一个二进制文件,然后在 RichEdit 控件中为我正在构建的一个小实用程序显示相应的结构,但没有找到其他方法。我已经知道如何读取二进制文件并将值读入struct

【问题讨论】:

  • 你的问题是富编辑还是输出结构的舒适?
  • 结构体......如果有一种简单的方法来迭代结构体,那就太好了。现在我必须单独引用每个字段,重复代码超过 100 行很烦人
  • @Roboto: 拥有 100 多个结构成员听起来本身就是个问题。也许你应该问一个关于如何重新设计该类的问题......
  • 结构体没有问题。它已经使用了 10 多年 - 没有问题。该结构包含一个 4 字节的二进制配置文件。
  • 其中一个问题是编译器知道结构成员的名称和顺序,但不会将该信息放入可执行文件中;用户也无法使用。

标签: c++ user-interface struct richedit c++builder-5


【解决方案1】:

BOOST_FUSION_ADAPT_STRUCT 似乎很适合这里。例如:

// Your existing struct
struct Foo
{
    int i;
    bool j;
    char k[100];
};

// Generate an adapter allowing to view "Foo" as a Boost.Fusion sequence
BOOST_FUSION_ADAPT_STRUCT(
    Foo,
    (int, i)
    (bool, j)
    (char, k[100])
)

// The action we will call on each member of Foo
struct AppendToTextBox
{
    AppendToTextBox(RichEditControl& Ctrl) : m_Ctrl(Ctrl){}

    template<typename T>
    void operator()(T& t)const
    {

        m_Ctrl.Lines.Append(boost::lexical_cast<std::string>(t));
    }

    RichEditControl& m_Ctrl;

};

// Usage:
void FillTextBox(Foo& F, RichEditControl& Ctrl)
{
    boost::fusion::for_each(F, AppendToTextBox(Ctrl));
}

【讨论】:

【解决方案2】:

如果我理解正确,原始问题的核心是如何迭代结构。简而言之,正如 Jerry Coffin 在评论中指出的那样,这是无法做到的。我会尝试解释原因,然后我会尝试解释如何做下一件最好的事情。

结构体作为单片数据存储在内存中,没有任何描述其结构的元数据。比如下面的结构:

struct Foo {
    char a;
    char b;
    char c;
    int i;
}

Foo f = {'x', 'y', 'z', 122};

可以在内存中使用十六进制表示法表示,如下所示

78 79 7A FF 7A 00 00 00

其中前 3 个字节包含 char 字段,第四个是用于填充的随机值,接下来的四个字节是整数 122 的小端表示。此布局因编译器和系统而异系统。简而言之,二进制表示不会告诉您数据是什么或各个字段的存储位置。

那么编译器如何访问结构中的字段?代码

char c = f.c;

被翻译成类似的指令

COPY BYTE FROM ([address of f] + 2) TO [address of c]

换句话说,编译器将字段的文字偏移量编码到代码中。同样,这对我们没有帮助。

因此,我们必须自己注释结构。这可以通过在结构内添加信息以将其转换为一种键值存储或通过添加第二个结构来完成。你不想改变原来的结构,所以第二个结构是要走的路。

我假设您的结构仅包含基本类型:int、char 等。如果您是结构中的其他复杂类,那么我建议在它们的基类中添加一个 ToString() 方法并调用该方法 - 那就是C# 和 Java 是如何做到的。

Foo tmp;

#define FIELD_OFFSET(f) ((char*)&(tmp.f) - (char*)&tmp)

enum FieldType { INT_FIELD, CHAR_FIELD, OBJECT_FIELD };

struct StructMeta {
    FieldType type;
    size_t offset;
};

StructMeta[] metadata = {
   {CHAR_FIELD, FIELD_OFFSET(a)},
   {CHAR_FIELD, FIELD_OFFSET(b)},   
   {CHAR_FIELD, FIELD_OFFSET(c)},
   {INT_FIELD, FIELD_OFFSET(i)},
   {OBJECT_FIELD, FIELD_OFFSET(o)},
}

void RenderStruct(Foo* f)
{
    for (int i = 0; i < sizeof(metadata)/sizeof(StructMeta); i++)
    {
        switch (metadata[i].type)
        {
             case CHAR_FIELD:
                 char c = *((char*)f + metadata[i].offset);
                 // render c
                 break;
             case INT_FIELD:
                 int i = *(int*)((char*)f + metadata[i].offset);
                 // render i
                 break;
             case OBJECT_FIELD:
                 Object* o = (object*)((char*)f + metadata[i].offset);
                 const char* s = o->ToString();
                 // render s
                 break;    
        }
    }
}

注意:所有指针运算都应在 (char*) 指针上完成,以确保将偏移量解释为字节。

【讨论】:

  • 这太棒了!如果结构也包含结构怎么办?我猜你必须有元数据的元数据?!
  • 我的建议是在结构或基类上实现 ToString() 方法。
  • 如何打印结构体的字段名?
【解决方案3】:

除非您构建自己的元数据来描述结构,否则无法迭代结构的成员。 C++ 编译器根本不会自动发出您需要的信息。

不过,借助一些宏魔法,您可以非常轻松地构建所需的元数据。很多年前我写了一些代码来做这个(实际上是一个完整的 Windows 自定义控件),我仍然一直在使用它。

基本技巧是使用一点宏魔法让编译器帮助您构建元数据。

// this is the structure I want to iterate
typedef struct {
   int foo;
   char bar[16];
} StructIWantToIterate;

// this is the metadata I need for each field of the structure
typedef struct {
   char * pszFieldName;
   size_t oFieldOffset;
   size_t cbFieldSize;
   int    eType;
} MyStructMeta;

// these are the field types I need to handle.
enum {
  type_is_int,
  type_is_char,
};

// these macros help to emit the metadata
#define NUMELMS(ary)     (sizeof(ary)/(sizeof(ary)[0]))
#define FIELDOFF(tag,fld)  ((size_t)&(((tag *)0)->fld))
#define FIELDSIZ(tag,fld)  sizeof(((tag *)0)->fld)
#define STDFLD(tag,fld,as)  #fld, FIELDOFF(tag,fld), FIELDSIZ(tag,fld), as

// now we declare the metadata for the StructIWantToIterate structure
#undef MYFLD
#define MYFLD(fld,as) STDFLD(StructIWantToIterate,fld,as)
static const MyStructMeta aMeta[] = {
   MYFLD(foo, type_is_int), // expands to "foo", 0, sizeof(int), type_is_int
   MYFLD(bar, type_is_char),// expands to "bar", sizeof(int), 16, type_is_char
};

// and when we want to do the iteration,  assume ptr is a pointer to an instance
// of StructIWantToIterate

for (int ii = 0; ii < NUMELMS(aMeta); ++ii)
{
   char szLine[100]; // pick your own worst case line size.

   // get a pointer to the current field within the struct
   void * pfld = ((byte*)ptr) + aMeta[ii].oFieldOffset;

   // print out the field data based on the type_is_xxx information
   switch (aMeta[ii].eType)
   {
      case type_is_int:
         sprintf(szLine, "%s : %d", aMeta[ii].pszFieldName, *(int*)pfld);
         break;

      case type_is_char:
         sprintf(szLine, "%s : %*s", 
                aMeta[ii].pszFieldName, 
                aMeta[ii].cbFieldSize, 
                pfld);
         break;
   }
   // send it to the richedit control
   RichEdit1->Lines->Append(asLine);    
}

【讨论】:

    【解决方案4】:

    没有办法遍历普通结构的成员。 您必须在结构声明之外提供此信息。

    正如之前的一些答案所示,您可以在编译时执行此操作。 但是,您也可以在运行时执行此操作。 这类似于某些“序列化”库的工作方式。

    你可能有以下课程:

    class MemberStore
    {
    public:
      template<typename Base>
      MemberStore(const Base &base) : 
        m_basePtr(reinterpret_cast<const char*>(&base))
      {}
    
      template<typename Member>
      MemberStore& operator&(const Member &classMember){
        DataInfo curMember;
        curMember.m_offset = reinterpret_cast<const char*>(&classMember) - m_basePtr;
        curMember.m_func = &CvtChar<Member>;
        m_members.push_back(curMember);
        return *this;
      }
    
      std::string convert(size_t index) {
        return m_members[index].m_func(m_basePtr + m_members[index].m_offset);
      }
    
      size_t size() const {
        return m_members.size();
      }
    
    protected:
      template<typename Type> 
      static std::string CvtChar(const void *data) {
        std::stringstream str;
        str << *reinterpret_cast<const Type*>(data);
        return str.str();
      }
    
    private:
      struct DataInfo {
        size_t m_offset;
        std::string (*m_func)(const void *data);
      };
      std::vector<DataInfo> m_members;
      const char *m_basePtr;
    };
    

    这个类包含一个“类成员”的向量(MemberStore::DataInfo),每个都有:

    • 从类基偏移。
    • 一种将它们转换为 std::strings 的方法。如果可以使用 std::stringstream 进行转换,则会自动生成此方法。如果不可能,应该可以专门化模板。

    您可以使用 & 运算符向此类添加元素(您可以连接多个 & 运算符)。之后,您可以迭代成员并使用其索引将它们转换为 std::string:

    struct StructureIWantToPrint
    {
      char a;
      int b;
      double c;
    };
    
    int main(int argc, wchar_t* argv[])
    {
      StructureIWantToPrint myData;
      myData.a = 'b';
      myData.b = 18;
      myData.c = 3.9;
    
      MemberStore myDataMembers(myData);
      myDataMembers & myData.a & myData.b & myData.c;
    
      for(size_t i=0;i<myDataMembers.size();++i) {
        std::cout << myDataMembers.convert(i) << std::endl;
      }
    
        return 0;
    }
    

    应该可以修改 MemberStore 类,以便自动将数据插入到 TextList 中,而不是存储将成员转换为 std::string 的方法。

    【讨论】:

      【解决方案5】:

      我不使用 C++ Builder,所以这方面的一些细节可能有点偏离,但总体思路至少应该相当接近:

      class richedit_stream { 
          TRichEditControl &ctrl;
      public:
          richedit_stream(TRichEditControl &trc) : ctrl(trc) {}
      
          template <class T>
          richedit_stream &operator<<(T const &value) {
              std::stringstream buffer;
              buffer << value;
              ctrl.Lines->Append(value.str().c_str());
              return *this;
          }
      };
      

      基本的想法很简单:一个richedit 控件的前端,它提供了一个模板化的操作符

      这确实有缺点——如果没有更多的工作,您将无法在数据转换为字符串时使用操纵器来控制数据的格式。由于它使用字符串流将事物转换为字符串,因此它可能也比您的代码显式编码每次转换的类型要慢一些。同时,您可以使用相当干净、简单、惯用的代码来换取相当少的投资。

      【讨论】:

      • 这很好,但我如何遍历结构来执行上述操作?
      • @Roboto:要避免对结构成员进行迭代,您无能为力,除非它们的类型相同,因此您可以使用数组。
      • 它们不是同一类型(int、uint、char 和 char * 的混合)
      • 不,你不能真正迭代一个结构,尽管有一些模仿可能有效。一种可能性是 Boost::tuple,它允许按索引指定成员,但只能在编译时(作为模板参数)。另一种方法是在外部生成代码——使用我提供的代码,您可以(例如)只有一个成员列表,并使用 awk 之类的东西来生成代码以显示它们。
      • 虽然你不能直接遍历struct,但是你可以写一个visitor方法把每个方法和它的名字发送给一个visitor 功能对象。这样做的好处是您可以为不同目的创建访问者,而无需修改原始对象的代码或结构(布局)。
      【解决方案6】:

      我建议创建用于写入文本框的模板方法:

      template <typename T>
      void
      Write_To_Textbox(const T& variable,
                       const std::string& variable_name,
                       TRichTextEdit & textbox)
      {
        //...
      }
      

      然后使用一些剪切、复制、粘贴和正则表达式功能替换编辑器功能并创建一个“注释”功能:

      void
      annotate(TRichTextEdit& textbox)
      {
        Write_To_Textbox(member1, "member1", textbox);
      //...
      }
      

      注意:检查模板函数的语法,因为我认为我在这个例子中没有做对。

      【讨论】:

      • P.S.添加#if 进行调试将阻止此代码在发布中。
      【解决方案7】:

      由于结构中有大量字段,请使用解析器或编写自己的解析器来生成源代码以打印成员、它们的名称和值。

      作为一个有趣的练习,在编写实用程序时自己计时。您可能会发现使用具有正则表达式搜索和替换功能的编辑器可能会更快。

      否则,请放弃您当前的设计并采用新的设计。我一直在使用记录和字段的设计。每条记录(结构)都有一个包含一个或多个指向Field_Interface 的指针的向量。 Field_Interfaceget_field_name()get_sql_data_type_text() 等方法。也不要忘记 Java 最喜欢的 toString(),它将字段值作为字符串返回。此技术允许您遍历字段容器并打印出它们的值(使用toString)和它们的名称(使用get_field_name())。

      添加 Visitor 模式进行读写(我称其为 Readers 和 Writers),您将拥有高度适应的字段和记录,而无需更改其内部内容。此外,这也很好地引入了通用编程,您可以在不知道它们的类型的情况下对字段和记录进行操作;或在叶级处理。

      顺便说一句,在您等待完美答案的时候,您可以编写一个函数来“迭代”或访问结构的成员。

      【讨论】:

      • 重新设计的问题是向后兼容性。通过重新设计来确保向后兼容性需要更多的时间和精力。这种格式已经使用了 10 多年,有时我们确实会遇到需要从 8 多年前升级的人,因此配置格式必须匹配,否则位将不再位于同一位置
      【解决方案8】:

      因此,您需要在运行时提供您的类型信息。此元数据在编译时可用,但随后被丢弃。我们只需要一种方法将其从编译器中拯救出来。

      1. 显式元数据,由 antonmarkov 和 John Knoeller 证明。您必须使其与结构保持同步,但它的优点是不会触及您的原始结构定义。

        1.1 代码生成 如果您的结构定义足够规则,您可以使用 awk 自动生成此元数据表。

      2. 元编程:如果您不介意重写结构(但保持布局不变,以便保持二进制兼容性),您可以让编译器为您完成繁重的工作.您可以使用Boost.tuple 声明您的结构,并使用Boost.Fusion 迭代其元素。

      【讨论】:

        【解决方案9】:

        并不是说我认为这是一个很好的答案,但出于完整性原因,感觉应该将其包括在内。一种稍微不同的方法是使用Windows debugger extension APIs 编写调试器扩展。您所描述的任务几乎非常适合调试器扩展。我说几乎是因为我不确定将它包含在发布版本中是一个非常好的计划。但是根据您需要此功能的位置,这可能是一种可能性。如果出于您自己的目的需要“内部”,它可能会起作用。如果它需要在客户的站点上运行,那么我不太愿意使用它,因为需要运送额外的行李(调试符号)。

        您的环境也存在一个很大的潜在问题。看来您使用的是 C++ Builder 版本 5。我不知道从该环境生成调试符号的方法可以与 Windows 调试工具一起使用。有一个实用程序 map2dbg 可以进行转换,但它显然至少需要 C++ Builder v6。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2014-02-22
          • 1970-01-01
          • 2017-09-23
          • 1970-01-01
          • 1970-01-01
          • 2021-11-10
          • 2012-09-19
          相关资源
          最近更新 更多