技术文章

了解最新技术文章

当前位置:首页>技术文章>技术文章
全部 146 常见问题 7 技术文章 139

think cell攻略:属性——隐藏线的隐藏状态

时间:2023-11-21   访问量:1039  标签: think cell,think-cell,属性

隐藏线的隐藏状态

上次我向您介绍了SFont部分定义(“混合”)字体的简单但有用的表示形式:

struct SFont {
    std::optional<MyString> m_ostrName;
    std::optional<int> m_onSize;
    std::optional<bool> m_obBold;};
清单 (1)

在我们的术语中SFont被称为“属性”,而它的成员m_ostrNamem_onSize则被m_obBold称为属性的“方面”。我们讨论了如何SFont将其视为表示一组字体或所有可能字体空间中的子空间,并利用这些见解来定义一些有用的方法和函数。

上一篇博客文章的重点是为您提供一些概念和观念,帮助您设计自己的属性,而不仅仅是字体。在我们的软件中,我们有很多遵循相同模式的属性:字体、颜色、填充、线条、项目符号、标记(在散点图和折线图中用于标记数据点),甚至精度(十进制数字的格式) 。今天我们将了解行格式与字体有何不同,以及如何修改简单的数据结构以使其适合行格式。SLineFormat让我们从基于 PowerPoint 的 LineFormat API的 的直接定义开始:

enum MsoLineDashStyle {
    msoLineSolid,
    msoLineDash,
    ...
    // Refer to Microsoft Office VBA documentation for the full
    // definition of MsoLineDashStyle supported in PowerPoint.}; enum MsoLineStyle {
    msoLineSingle,
    msoLineThickBetweenThin,
    ...
    // Refer to Microsoft Office VBA documentation for the full
    // definition of MsoLineStyle supported in PowerPoint.};struct SLineFormat {
    std::optional<bool> m_obVisible;
    std::optional<int> m_onWeight; // line thickness
    std::optional<MsoLineDashStyle> m_omsolinedashstyle;
    std::optional<MsoLineStyle> m_omsolinestyle;
    // add MsoLineCapStyle, MsoLineJoinStyle, ... and some
    // representation of fill or color as needed};
清单 (2)

看起来很简单,但是看不见的线的破折号样式的含义是什么?权重为 0(零)的“可见”线与“不可见”线有何不同?满足隐藏状态的概念:我们将隐藏状态定义为在数据结构中表示和维护的状态(信息),但用户无法辨别。每个设计数据结构的人都经常遇到隐藏状态,无论他们喜欢与否。在许多情况下,人们很容易为了一些表面上的简单性和明显的对称美而忽略隐藏状态,或者仅仅是因为开发人员未能识别出后来证明是隐藏状态的东西。

隐藏状态不一定是坏事:考虑使用某种特意选择的字体在 PowerPoint 文本框中键入文本。当由于某种原因您决定重写文本时,第一步可能是删除现有文本并返回到空文本框。空文本框没有任何字体的视觉表示(不考虑闪烁光标的大小),但您期望当您开始键入时,您的文本将使用之前使用的相同字体。这种行为很有用,实现它需要刻意使用隐藏状态。

但在大多数情况下,隐藏状态更令人困惑而不是有帮助。通常,隐藏状态以数据模型历史遗迹的形式出现。在我们的产品领域中,如果我们不小心隐藏状态,两个图表可能看起来“像素级”相同,但当用户更改基础数据时,可能会表现出非常不同的行为。如果用户从第三方收到这些图表并且无法知道它们是如何创建的,这尤其令人恼火。

“无用的隐藏状态”的实例通常被称为“冗余”,数据结构设计的一般经验法则是冗余是不好的,应该避免。我们是否忽略了 中的任何隐藏状态SFont不,我们没有:我们对SFont财产的定义的所有方面都是相互独立的。每个方面本身都是有意义的,无论其他方面的价值如何。借用向量空间的概念,我们说字体的名称、大小和粗细是相互正交的。

回到SLineFormat,我们发现一条不可见的线有多种表示形式。鉴于所有不可见线对用户来说看起来都是一样的,我们可以有把握地说这是隐藏状态的一个实例。此时我们需要确定这个特定的隐藏状态是有用还是有害。我可以立即说出一些常见的用例,在这些用例中,不可见线的模糊表示是有害的:

同时,我发现很难提出任何用例来证明以不可见行格式维护隐藏状态是合理的。因此,为了本文的目的,我们假设SLineFormat不应包含任何隐藏状态。

我们怎样才能做到这一点?首先,成员m_obVisible和存在冗余m_onWeight它们中的任何一个都可以指示不可见的行格式,因此理想情况下只需要其中一个。显然,m_onWeight是表示可见行格式所必需的,但m_obVisible不会提供任何无法从 获得的信息m_onWeight,因此m_obVisible应该被删除:

struct SLineFormat {
    std::optional<int> m_onWeight; // line thickness, 0 means invisible
    std::optional<MsoLineDashStyle> m_omsolinedashstyle;
    std::optional<MsoLineStyle> m_omsolinestyle;}
清单 (3)

微软MsoLineDashStyleMsoLineStyle枚举没有“不可见”或“无线”的值,但仍然存在不可见线的冗余表示。那个怎么样?好吧,如果一个线格式有m_onWeight==0,即它代表一条不可见的线,m_omsolinedashstyle并且m_omsolinestyle可能仍然有值。因此 的成员中有很多不同的值组合SLineFormat,它们都可以表示“no line”。

我们怎样才能控制这些无意义的价值观组合呢?至少有两种可能的方法:

  1. 我们可以忽略其中的任何值m_omsolinedashstyle以及m_omsolinestyle每当我们遇到时m_onWeight==0(参见清单 4),

  2. 或者我们可以确保每当 时m_onWeight==0,其他成员始终具有一个且相同的唯一值(参见清单 5)。

#define IS_EQUAL_ASPECT(member) ( \
    static_cast<bool>(lhs.member)==static_cast<bool>(rhs.member) \
    && (!lhs.member || *lhs.member==*rhs.member) \) // same as for SFontbool SLineFormat::IsInvisible() const& noexcept {
    // Note that !IsInvisible() does not necessarily mean "visible":
    // If !m_onWeight, then visibility is undefined/mixed.
    return m_onWeight && 0==*m_onWeight;}bool operator==(SLineFormat const& lhs, SLineFormat const& rhs) noexcept {
    return IS_EQUAL_ASPECT(m_onWeight)
        && (
            lhs.IsInvisible()
            || (
                IS_EQUAL_ASPECT(m_omsolinedashstyle)
                && IS_EQUAL_ASPECT(m_omsolinestyle)
            )
        );}
清单 (4)
 
void SLineFormat::AssertInvariant() const& noexcept {
    if( m_onWeight ) {
        if( 0==*m_onWeight ) {
            std::assert( !m_omsolinedashstyle );
            std::assert( !m_omsolinestyle );
        } else {
            std::assert( 0 <= *m_onWeight );
        }
    }
    // For more details about how we deal with unexpected conditions in our
    // code, watch Arno's talk "A Practical Approach to Error Handling".}bool operator==(SLineFormat const& lhs, SLineFormat const& rhs) noexcept {
    AssertInvariant();
    return IS_EQUAL_ASPECT(m_onWeight)
        && IS_EQUAL_ASPECT(m_omsolinedashstyle)
        && IS_EQUAL_ASPECT(m_omsolinestyle);}
清单 (5)

虽然忽略不相关方面的值在理论上听起来很简单且最规范,但在实践中事实证明,确保无意义方面的特定值可以实现更干净、更简单、更规范的代码。干净、简单、规范的代码意味着更少的错误和更好的可维护性,因此我们在我们的软件中主要使用后一种方法。特别是,由于我们属性的设计方式,std::nullopt无论如何,每个方面都方便地具有“未定义”( ) 状态。为此目的使用“未定义”状态可以使我们免于处理任意魔法值(当然,原则上这也是可行的)。

有了对依赖(非正交)属性方面的基本了解,让我们看看其他一些关键方法和函数的实现SLineFormat

#define SET_ASPECT(member) if(rhs.member) member = rhs.member // same as for SFontvoid SLineFormat::operator<<=(SLineFormat const& rhs) & noexcept {
    SET_ASPECT(m_onWeight);
    if( IsInvisible() ) {
        m_omsolinedashstyle = std::nullopt;
        m_omsolinestyle = std::nullopt;
    } else {
        SET_ASPECT(m_omsolinedashstyle);
        SET_ASPECT(m_omsolinestyle);
    }
    AssertInvariant();}SLineFormat operator<<(SLineFormat lhs, SLineFormat const& rhs) noexcept {
    lhs <<= rhs;
    return lhs;} // analogous to SFont
清单 (6)

现在,我们可以使用 (6)operator<<(...)来编写链式表达式,就像使用 那样SFont,它简单、优雅,但正如我们将在下面看到的,它是错误的(引自 HL Mencken)。举个例子,让我们再次看看我们的产品域:我们有一个可见线的全局默认线格式,并且我们有一个用于特定目的的部分定义的默认线格式的层次结构。我们如何为条形图中突出显示的部分的轮廓编写默认值?

auto const lineHighlight = lineDefaultGlobal    << lineDefaultSegment << lineDefaultHighlight; // WRONG!
清单 (7)

这个表达式非常适合SFont,那么用它来做什么有什么问题SLineFormat呢?假设我们从一些合理的全局默认行格式开始。一些现代的图表样式使用没有轮廓的彩色区域,因此lineDefaultSegment可以设置为明确的不可见轮廓。突出显示一个片段可能会证明一个特别粗的轮廓(甚至可能是红色的,但我们的玩具示例中不支持颜色),并且它应使用全局默认值中定义的任何内容MsoLineDashStyleMsoLineStyle因此,我们的默认值可能如下所示:

SLineFormat const lineDefaultGlobal{
    /*m_onWeight*/ 6,
    /*m_omsolinedashstyle*/ msoLineSolid,
    /*m_omsolinestyle*/ msoLineSingle};SLineFormat const lineDefaultSegment{
    /*m_onWeight*/ 0,
    /*m_omsolinedashstyle*/ std::nullopt,
    /*m_omsolinestyle*/ std::nullopt};SLineFormat const lineDefaultHighlight{
    /*m_onWeight*/ 12,
    /*m_omsolinedashstyle*/ std::nullopt,
    /*m_omsolinestyle*/ std::nullopt};
列表 (8)

如果我们将清单 (8)中的值输入表达式 (7)中,则结果是部分定义的行格式:{/monWeight/ 12, /momsolinedashstyle/ std::nullopt, /m_omsolinestyle/ std::nullopt}显然,这没有用:为了在突出显示的段周围显示一条线,仅知道粗细是不够的。我们还需要一些明确定义的MsoLineDashStyleMsoLineStylemsoLineSolid到底在哪里msoLineSingle迷路lineDefaultGlobal了?答案是:结合性。

C++ 状态的运算符优先级规则<<是从左到右计算的。如果我们在操作链中遇到一种不可见的行格式<<那么根据清单(6),我们清除所有隐藏状态。然后,当我们使用另一种行格式(部分定义可见)时,我们无法从最左边的操作数恢复丢失的方面。这很容易解决:我们需要从右到左评估链:

auto const lineHighlight = lineDefaultGlobal	<< (lineDefaultSegment << lineDefaultHighlight); // correct


上一篇:think cell攻略:属性——为字体世界建模

下一篇:think cell博客:tc::改变

发表评论:

评论记录:

未查询到任何数据!

免费通话

24小时免费咨询

请输入您的联系电话,座机请加区号

免费通话

微信扫一扫

微信联系
返回顶部