65.9K
CodeProject 正在变化。 阅读更多。
Home

列定义控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.64/5 (5投票s)

2002年7月9日

12分钟阅读

viewsIcon

69528

downloadIcon

1553

提供一个控件,用于在导入文本文件到您的应用程序时指定列的位置。

目录

引言

大多数应用程序的主要需求之一就是能够从持久化存储中加载数据。通常,这是通过使用预定义格式的文件(可能是 XML)来实现的。然而,许多应用程序的一个重要特性是能够导入非应用程序主要使用格式的数据,在大多数情况下,这些数据可能来自普遍存在的格式,如纯文本(本文将讨论的)。

通常,包含记录数据的文本文件有两种形式:一种是列宽度固定,另一种是列由预定义字符分隔。本文介绍了一个控件(如图所示),它允许用户在具有固定宽度列的表格数据文本文件(第一种情况)上插入列标记,然后将此数据转换为类数组的数据结构,以便您的应用程序进行处理。

该项目主要分为两个部分:

  1. 固定宽度列定义控件 CFixedWidthColsFileCtrl
  2. 用于实际处理的 CFileContents

使用代码

该代码主要有两种使用方式:

  1. 使用本文介绍的列定义控件提供的完整用户界面。
  2. 不使用用户界面,例如,在不重新定义列的情况下再次导入文件,使用本文最后介绍的 CFileContents 类。

由于本文的重点是该控件,我们将更详细地研究第一种用法,但也会简要讨论如何在不使用用户界面的情况下使用该代码。

您可以根据需要使用该控件,例如作为对话框的一部分,使用标准技术。由于导入文本文件对用户来说可能非常令人困惑,我们认为向导是解决此问题的最佳方法,演示项目就是这样做的。实现向导式对话框的细节超出了本文的范围,但您可能会发现 A N Other 撰写的文章“blah blah blah”很有用。将控件放置在对话框或属性页上的方法如下所示。

使用该控件,方法一

  1. 首先,您需要获取用户希望导入的文本文件的文件名。这可以通过演示项目中所示的方法来完成,或者您可能已经知道文件名。您需要将以下文件添加到您的项目中:

    • FileContents.cppFileContents.h
    • FixedWidthColsFileCtrl.cppFixedWidthColsFileCtrl.h

    这将向您的项目添加两个类:

    CFileContents 此类用于实际处理文件,并且可以在不使用 CFixedWidthColsFileCtrl 类的情况下用于批量处理或避免用户界面组件。
    CFixedWidthColsFileCtrl 此类提供了演示项目向导第二页所示的用户界面。它以类似编辑控件的方式显示给定文件,并允许用户单击显示屏以插入列标记,然后使用这些标记处理文件并整理数据。

  2. 接下来,您需要创建一个对话框或属性页,在其中放置控件,并插入一个自定义控件对象,该对象将在应用程序运行时被 CFixedWidthColsFileCtrl 替换。您需要将控件的“类”属性设置为 MFCFixedWidthColsFileCtrl

    另一个重要步骤是设置自定义控件的 Style 属性为 0x50b30000,这对应于以下标志:WS_VISIBLE | WS_CHILD | WS_BORDER | WS_TABSTOP | WS_GROUP | WS_VSCROLL | WS_HSCROLL。虽然不这样做似乎不会引起问题,如果您只使用标准对话框,但在属性页或向导中存在问题,通过正确设置样式位可以解决这些问题。

    当然,这不是正确的做法。然而,尝试在 PreCreateWindow 中使用 ModifyStyleModifyStyleEx,由于某种原因,似乎无法解决问题。当然,这留下这些常量值可能更改的可能性(尽管不太可能),但似乎没有其他办法。

  3. 现在,使用 ClassWizard,我们可以为这个对话框创建一个类(只需双击对话框即可打开 ClassWizard)。

  4. 下一个步骤是创建对话框类中的一个成员变量,该变量代表列定义控件。为此,首先在对话框的 .h 文件的顶部添加以下行:

    #include "FixedWidthColsFileCtrl.h"

    最后,在您的类定义中,添加以下声明:

    CFixedWidthColsFileCtrl m_ctrlFixedWidthColsFile;
  5. 我们需要通过将以下内容添加到 DoDataExchange 函数中来确保控件占位符被上述控件实例替换(将 IDC_CUSTOM_FIXED_WIDTH_COLS_FILE 更改为您在第 2 阶段定义的控件的 ID)。

    DDX_Control(pDX, IDC_CUSTOM_FIXED_WIDTH_COLS_FILE, m_ctrlFixedWidthColsFile);
  6. CFileContents 类用于执行实际的制表操作,并由该控件使用,因此需要创建一个此类的实例作为对话框类的成员变量,或者在创建对话框时传递其指针/引用。后一种方法通常是更好的选择,可以通过修改构造函数声明为以下内容来实现:

    CMyDialogClass(CFileContents &fileContents);

    现在,需要在对话框类定义中添加一个成员变量来存储传入的对象。

    CFileContents &m_FileContents;

    最后,在构造函数定义中,我们需要初始化这个变量。

    // if you've created a dialog:
    CMyDialogClass::CMyDialogClass(CFileContents &fileContents, CWnd *pParent) : 
                                 CDialog(...), m_FileContents(fileContents)
    
    // if you've created a property page:
    CMyPropPageClass::CMyPropPagePClass(CFileContents &fileContents) : 
                                 CPropertyPage(...), m_FileContents(fileContents)
  7. 在调用 CFixedWidthColsFile 类的 LoadFile 方法之前,您希望显示的该文件必须已经读取到传入的 CFileContents 对象中。可以使用类似以下的代码来实现这一点:

    CFileContents fileContents;
    CMyDialogClass dlgMyPage(fileContents);
    
    // load the text file given by csFilename into the FileContents class:
    if (!fileContents.ReadFile(csFilename))
    {
        // an error has occurred.
    }
    else
    {
        // display the dialog:
        dlgMyPage.DoModal();
    }

    有关完整示例,请参阅演示项目,该项目使用了一个向导。在这种情况下,CFileContents 类作为应用程序类的一部分创建(尽管显然在完整的应用程序中,它要么是局部变量,要么是其他类的一部分),然后传递给向导属性页。然后,向导的第一页从用户那里获取文件名,并将选定的文件读取到 CFileContents 类中。

  8. 当调用 OnInitDialog(对话框情况)或 OnSetActive(属性表情况)函数时,控件需要处理我们 m_FileContents 类读取的文件。这通过 LoadFile 函数实现。

    ctrlFixedWidthColsFile.LoadFile(&m_FileContents);

    注意:在调用 CFixedWidthColsFile::LoadFile 方法之前,您希望显示的该文件必须已经读取到传入的 CFileContents 对象中。

  9. 当对话框关闭时,我们需要根据用户使用控件标记的列来整理数据。我们可以通过 PopulateFields 函数来实现这一点,只需在 OnOK(对话框情况)或 OnWizardNext(向导情况)函数中添加一行代码。

    ctrlFixedWidthColsFile.PopulateFields();

在将控件放置到对话框以及将其加载到文件中方面,我们需要做的就是这些。当对话框或向导页面结束时,在初始化期间传入的 CFileContents 对象现在将包含一个完全整理好的数据集,可以通过 GetField参见参考)函数进行访问,并转换为更适合您特定应用程序的格式。

此类的一个小限制是它不提供字段的类型分类。因此,所有数据都存储在 CFileContents 对象内的 CString 中,这意味着您需要自己执行类型转换,但这不应该造成太多问题。

在没有 UI 的情况下使用 CFileContents 类,方法二

上述方法的一个替代方案是单独使用 CFileContents 类。这提供了一种将列分隔的文本文件转换为制表数据结构而无需任何用户交互的方法。

在此模式下使用时,使用方法与上面描述的类似,除了两个主要因素:

  1. 不涉及 CFixedWidthColsFile 类,因此您无需将相应的 .h/.cpp 文件添加到您的项目中。
  2. 所有“操作”通常在一个函数中进行,甚至(在某些情况下)可能在一个单独的工作线程中进行,尽管该类尚未在此场景中进行测试,并且不包含固有的线程安全性。

以下是基本方法:

  1. 首先,您需要创建或选择一个函数来执行必要的处理。在作用域内,您需要能够访问要处理的文件的文件名,以及分隔数据的列位置。

    在文件顶部,您需要包含 FileContents.h 文件。

    #include "FileContents.h"
  2. 接下来,您需要创建 CFileContents 类的一个实例,并将所需文件读取到该实例中。

    CFileContents fileContents;
    fileContents.ReadFile(csMyFilename);
  3. 最后,需要处理该文件。为了进行处理,您需要构建一个列分隔符位置的列表。这通过 CDWordArray 传递给 CFileContents 类的 PopulateFields 函数。以下是一些将列标记为从字符位置 0、12、30 和 60 开始的示例代码:

    CDWordArray colPositions;
    colPositions.SetSize(3);     // note that this will imply FOUR columns
    
    colPositions.SetAt(0, 12);
    colPositions.SetAt(1, 30);
    colPositions.SetAt(2, 60);

    此数组现在可以传递给 PopulateFields 函数。

    fileContents.PopulateFields(&colPositions);

和以前一样,数据现在已经处理完毕,可以通过该函数其余部分使用 CFileContentsGetField参见参考)函数来访问。

参考

下面将描述 CFixedWidthColsFileCtrl 类和 CFileContents 类的公共接口。

CFixedWidthColsFileCtrl : CWnd

函数 描述
CFixedWidthColsFileCtrl( )

构造一个 CFixedWidthColsFileCtrl 对象。

参数:

返回值:

LoadFile(...) CFileContents 对象中的文件加载到控件中以供显示。这是用户将选择列位置的文件。

参数: CFileContents *pFileContents - 表示要加载到控件中的文件的对象。指针必须非 NULL,并且指向一个 CFileContents 类,该类的 ReadFile() 函数已调用且文件已成功加载。

返回值:

PopulateFields( ) 此方法调用控件正在操作的 CFileContents 对象的 PopulateFields(...) 函数,并将用户定义的列传递给它。它会导致 CFileContents 对象中的数据被制表。如果多次调用此函数,数据将被重新制表。

参数:

返回值:

CFileContents

函数 描述
CFileContents( ) 构造一个 CFileContents 对象。

参数:

返回值:

ChangeTabSize(...) 设置处理文件时使用的制表符大小。调用 PopulateFields 方法时,所有制表符都会展开,此函数设置相当于一个完整制表符的空格数。

参数: int nTabSize - 新的制表符大小

返回值:

GetField(...) 检索存储在字段中的数据。

参数: int nLine - 要检索字段的行号(类似于表中的行),从零开始。
int nField - 要检索行的字段编号(类似于表中的列),从零开始。

返回值:一个 CString,包含检索到的数据。

GetLine(...) 检索整行,就像最初读取的那样。请注意,如果类处于固定宽度列模式(目前唯一可用的模式),此函数还将扩展行中的制表符。

参数: int nLine - 要检索的行号 - 从零开始。

返回值:一个 CString,包含展开制表符后的检索行。

GetMaxLineLength() 返回从文本文件中导入的最长行的长度。

参数:

返回值:一个 int,包含从文本文件中读取的最长行的长度。

GetNumberOfLines() 返回由 ReadFile(...) 读取的文件中的行数。

参数:

返回值:一个 int,包含从文件中读取的行数。

PopulateFields(...) 此函数是该类的最重要的部分之一。它使用通过 pColumns 参数传入的列分隔符位置来制表文件数据。

参数: CDWordArray *pColumns - 一个数组,包含文件中列分隔符的位置。值应从零开始,并且不应包含第一个列的 0 值。因此,如果数组中有 n 个值,将生成一个包含 n + 1 列的表。数组中的值是下一列的第一个字符位置,例如,如果数组包含一个值为 5 的项,则第一列的列将在 0 和 4 之间,第二列的列将在 5 及以后的位置。

返回值:

ReadFile(...) 此函数再次是该类的最重要的部分之一。它接受一个文件名并将文件数据读入内存以供处理。

参数: CString csFileName - 要加载的文件名。
bool bFixedWidthCols = true - 此参数的默认值为 true,应省略,因为其预期功能尚未实现(见下文)。

返回值:一个 bool,指示成功或失败。

未来改进 / 待办事项

可以在此处提供的类中添加的一项功能是能够基于字符分隔符而不是固定宽度列来制表文件。这就是 CFileContentsReadFile 方法中 bFixedWidthColumns 参数的原因。该类之前以类似的方式使用过,这里是来自使用此类程序的截图,以帮助您了解向导/对话框页面的可能布局。

致谢

本文的源代码完全由 Cathy 编写。以下人员在某个方面有所帮助或贡献:

  • CFECFileDialogCFileEditCtrlCPJAImage 是 PJ Arends 编写的类,他也帮助解决了代码最初的一些问题。
  • CDynamicItems 是由 David Excoffer 编写的。

结论

我们希望此代码有所帮助。据我们所知,代码没有问题/错误,但请随时通过本文底部的评论部分或发送电子邮件至 Cathy ( CatherinePelham@msn.com ) 告知我们错误/修复 - 欢迎建设性的批评。

更新

  • 2002 年 7 月 12 日 - 源文件已更新。
© . All rights reserved.