跳转至

开发规范

命名规则

总体原则

  • 全局变量、全局函数、宏、静态变量:使用下划线分隔的命名风格。
  • 其余所有标识符(类、函数、局部变量、成员变量、参数等):统一采用驼峰命名法(CamelCase)。
  • 命名应遵循 IDE 最优搜索原则,便于开发者在输入时快速联想并定位目标符号(如类、函数、枚举等)。

具体规范

类型 命名格式 示例
全局变量 g_ + 下划线命名 g_total_count
静态变量(函数内或类内) s_ + 下划线命名 s_exec_count
全局函数 全小写 + 下划线分隔 this_is_global_function()
全大写 + 下划线分隔 MAX_BUFFER_SIZE
类名 首字母大写的驼峰命名 ExampleClass
枚举 首字母大写的驼峰命名,枚举应以统一前缀开头 DockingAreaWorkFlowOperate
公有成员变量 小写开头的驼峰命名,无前缀 publicMemberValue
私有成员变量 m + 小写开头的驼峰命名(不加下划线 mResultValue
成员函数 / 局部变量 / 参数 小写开头的驼峰命名 functionOne, argOne, loopCount
命名空间 简洁、全大写缩写(避免过长) DA(而非 DataAnalysis

说明

谷歌规范推荐私有成员变量使用后缀下划线(如 value_),虽可避免与局部变量混淆,但不利于 IDE 搜索。而传统 m_ 前缀在 Pimpl 模式下显得冗余(如 d_ptr->m_value
本规范采用 m 前缀 + 驼峰(如 mValue),兼顾简洁性与 IDE 提示体验:在 Pimpl 中写作 d_ptr->mValue 更美观,且输入 d_ptr->m 即可触发 IDE 自动补全

示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int g_total_count;//全局变量以g开头,下划线命名

class ExampleClass{//类名以驼峰命名,且首字母大写
public:
    ExampleClass();
    void functionOne(int argOne,double argTwo);//类函数首字母小写,驼峰命名 ;变量参数驼峰命名,首字母小写
    virtual void virtualFunction();
public:
    double publicMemberValue;//类公有成员变量不做前缀限定
private:
    double mResultValue;//类私有成员变量以m开头,驼峰命名
};

void ExampleClass::functionOne(int argOne,double argTwo){
    static int s_exec_count = 0;//静态参数s_开头,下划线命名
    int loopCount = argOne;// 局部变量小写开头,驼峰命名
}

void this_is_global_fucntion();//全局函数以下划线分隔
void this_is_global_fucntion(){
    int loopCount = 0;//局部变量小写开头,驼峰命名
}

/**
 * @brief 定义了固定的dock窗口
 * 
 * 下面枚举的命名,使用了以IDE最优搜索原则,开发者输入Dock,即可联想到所有的枚举,
 * 如果命名为WorkFlowOperateDockingArea,DataOperateDockingArea这样不利于IDE列举出枚举
 */
enum DockingArea
{
    DockingAreaWorkFlowOperate,
    DockingAreaDataOperate,
    DockingAreaChartOperate,
    DockingAreaWorkFlowManager,
    DockingAreaDataManager,
    DockingAreaChartManager,
    DockingAreaSetting,
    DockingAreaMessageLog
};

说明

QStyleOption*系列就是比较好的例子,所有相似功能的类都是以QStyleOption打头,例如QStyleOptionToolButton,而不是命名为QToolButtonStyleOption

函数命名规范

属性命名规

  • 普通属性:使用 setXxx() / getXxx() 配对。不要省略 get,以利于 IDE 搜索。
  • 返回引用的属性不加 get,直接命名为 xxx()
  • 布尔属性
  • 可设置性(capability):setXxxable(bool),如 setSelectable(bool)
  • 当前状态(state):isSelected() / setSelected(bool)
  • 读取函数统一以 is 开头。
  • 统一使用 set 前缀,使 IDE 能通过输入 set 联想所有可配置项。

总之尽量使用set,让编译器能通过set联想出所有可设置属性,用getis能联想出所有可读属性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class ExampleClass{
public:
    void setText(const std::string& v);//设置值以set开头
    std::string getText() const;//获取值以get开头,且返回值
    //以下是返回引用
    std::string& text();//返回引用的不写get
    const std::string& text() const;//返回引用的不写get
    //能否被选中
    void enableSelect(bool on);//可以但不推荐,某些场合下enable更好理解可以使用
    void setSelectable(bool on);//推荐
    bool isSelectable() const;
    //是否选中了
    void setSelect(bool on);
    bool isSelected() const;
}

虚函数重写

  • 重写虚函数时,必须使用 override
  • 建议保留 virtual 关键字,以增强可读性,明确其为虚函数。
1
2
3
4
5
class ChildClass : public ExampleClass{
public:
    ChildClass();
    virtual void virtualFunction() override;//虚函数后面接override,为了更好发现,前面的virtual关键字不省略
};

注释规范

遵循 Doxygen 注释风格

  • 头文件中
  • 类成员函数不写完整 Doxygen 注释(避免因文档变更触发全量构建);
  • 但需保留简短普通注释,内容等同于 @brief
  • get/set 配对函数可共用一条注释;
  • 针对Qt的信号(signals) 没有 .cpp 实现,可以在头文件中写详细的 Doxygen 注释。
  • 源文件(.cpp)中
  • 所有函数实现前写完整 Doxygen 注释;
  • 注释前空一行
  • 使用 /** ... */ 格式;
  • 成员变量注释使用 ///<

示例:(//!为示例的说明)

h文件注释示范

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class DAAbstractNode{
    /////////////示例的说明////////////////
    //! 头文件中不写doxygen注释
    //! get/set配对的可以写一个注释
    //////////////////////////////////////


    //获取图标,图标是节点对应的图标
    QIcon getIcon() const;
    void setIcon(const QIcon& icon);
    //获取节点的元数据
    const DANodeMetaData& metaData() const;
    DANodeMetaData& metaData();
    //说明
    QString getNodeTooltip() const;
    void setNodeTooltip(const QString& tp);
    //设置元数据
    void setMetaData(const DANodeMetaData& metadata);
    //返回自身智能指针
    SharedPointer pointer();
    // id操作
    IdType getID() const;
    void setID(const IdType& d);
};

class DAWorkFlow{
    /////////////示例的说明////////////////
    //! 信号没有cpp的实现,可以把doxygen注释写在头文件中
    //////////////////////////////////////
signals:
    /**
     * @brief 节点创建产生的信号
     * @param node
     */
    void nodeCreated(DAAbstractNode::SharedPointer node);
    /**
     * @brief 节点添加的信号
     * @param node
     */
    void nodeAdded(DAAbstractNode::SharedPointer node);
};

cpp文件注释示范

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/**
 * @brief 获取节点的说明
 * @return 返回说明字符串
 * @sa setNodeTooltip
 */
QString DAAbstractNode::getNodeTooltip() const
{
    return (d_ptr->_meta.getNodeTooltip());
}
            <--注意doxygen注释前空一行
/**
 * @brief 设置节点的说明
 * @param tp 说明文本
 * @sa getNodeTooltip
 */
void DAAbstractNode::setNodeTooltip(const QString& tp)
{
    d_ptr->_meta.setNodeTooltip(tp);
}

调试与功能开关

  • 对于大段功能的开关,禁止使用注释屏蔽代码
  • 应使用 宏开关 控制调试输出或功能启用。

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
 * @def 此为1将开启打印辅助信息
 */
#define DA_DAABSTRACTNODE_DEBUG_PRINT 0

int DAAbstractNode::getInputNodesCount() const
{
    int res = 0;
#if DA_DAABSTRACTNODE_DEBUG_PRINT
    qDebug() << getNodeName()
             << "-> getInputNodesCount()\n"
                "    _linksInfo:";
#endif
    for (const DAAbstractNodePrivate::LinkData& d : qAsConst(d_ptr->_linksInfo)) {
#if DA_DAABSTRACTNODE_DEBUG_PRINT
        qDebug() << "    outputKey=" << d.outputKey << "(" << d.outputNode.lock()->getNodeName()
                 << ")--->inputKey=" << d.inputKey << "(" << d.inputNode.lock()->getNodeName() << ")";
#endif
        SharedPointer toNode = d.inputNode.lock();
        if (toNode.get() == this) {
            //说明这个是其他节点连接到自身的item
            ++res;
        }
    }
    return res;
}

包括功能的调整,禁止使用注释而应该使用宏

编码禁止列表

  • ❌ 禁止头文件中使用using namespace
  • ❌ 基类若用于继承,析构函数必须为 virtual
  • 库导出类禁止继承非模板的 STL 或 Qt 模板类(如 class DLLEXPORT X : public QList<QColor>),因可能导致符号冲突或析构不完整。若需继承模板类,自身也应为模板类。

语法规范性条例

下面这些内容是涉及一些语法相关的规范和条例,通用规范条例常见effective cpp和effective modern cpp

用异常而不是用std::optional

早期c++规范都不推荐使用异常,例如谷歌的条款,而是推荐使用错误码,现代C++中,更为推荐使用异常,主要有如下原因:

  • 异常可携带更多的信息,甚至结合Qt的多语言框架,能直接进行多语言的异常信息携带
  • std::optional在某些场合能替代异常,但是有很大的缺陷,首先无法携带信息,除非用一个类似getLastErrorString的函数来获取,实际工程上大规模使用并不友好
  • 宁愿用错误码也不应该用std::optional,在和界面相关的编程中,错误码至少能进行更详细的错误信息描述
  • 如果使用了异常,开发过程要注意处理,尤其和一些不使用异常的库混用的时候,例如Qt
  • 一些科学计算类、io操作类的库建议使用异常

操作符重载

操作符重载函数必须定义在所属类的命名空间内,以支持 ADL(Argument-Dependent Lookup)。

✅ 如下是正确的做法:

1
2
3
4
5
6
7
8
namespace DA
{
class DANodeLinkPoint
{
}

bool operator==(const DANodeLinkPoint& a, const DANodeLinkPoint& b);
}// end namespace DA

❌ 如下是错误的做法:

1
2
3
4
5
6
7
8
namespace DA
{
class DANodeLinkPoint
{
}
}// end namespace DA
//操作符重载不应该定义在命名空间以外
bool operator==(const DA::DANodeLinkPoint& a, const DA::DANodeLinkPoint& b);

Qt编程注意事项

不同屏幕缩放比例下QPixmap绘制问题

在高 DPI 或缩放比例 ≠ 100% 的环境下,QPixmap 的物理尺寸与逻辑尺寸不一致。
必须除以 devicePixelRatio() 获取逻辑尺寸,否则绘制位置会偏移。

1
2
3
4
5
6
QPixmap mPixmap;
QIcon mIcon;

...

mPixmap = mIcon.pixmap(getBodySize().toSize());

在paintEvent里绘制,这里演示是要把pixmap绘制到bodyRect的中心,下面是错误的绘制方法

❌ 如下是错误的做法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void xxx::paintBody(QPainter* painter,
                    const QStyleOptionGraphicsItem* option,
                    QWidget* widget,
                    const QRectF& bodyRect)
{
    painter->save();
    QRect pixmapRect = mPixmap.rect();
    // 计算位图绘制的起始点
    int xoffset = bodyRect.x() + (bodyRect.width() - pixmapRect.width()) / 2;
    int yoffset = bodyRect.y() + (bodyRect.height() - pixmapRect.height()) / 2;
    painter->setRenderHint(QPainter::SmoothPixmapTransform, true);
    painter->drawPixmap(QPoint(xoffset, yoffset), mPixmap);
    painter->restore();
    ...
}

上面方法之所以错误,是因为没有考虑devicePixelRatio,在缩放比例为100%时,上面代码绘制的图片没有问题,但缩放比例不是100%,上面的代码绘制出来是不正确的位置,主要异常在xoffset和yoffset,因为pixmapRect.width()/height()并非QPixmap在当前绘图坐标系下的宽高,这时两个坐标系是不一致的,你要获取当前绘图坐标系下的宽高,需要除以devicePixelRatio

✅ 如下是正确的做法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void xxx::paintBody(QPainter* painter,
                    const QStyleOptionGraphicsItem* option,
                    QWidget* widget,
                    const QRectF& bodyRect)
{
    painter->save();
    QRect pixmapRect = mPixmap.rect();
    qreal dr         = mPixmap.devicePixelRatio();
    // 注意在不同屏幕缩放比例下,pixmap的实际尺寸要除以devicePixelRatio
    int xoffset = bodyRect.x() + (bodyRect.width() - pixmapRect.width() / dr) / 2;
    int yoffset = bodyRect.y() + (bodyRect.height() - pixmapRect.height() / dr) / 2;
    painter->setRenderHint(QPainter::SmoothPixmapTransform, true);
    painter->drawPixmap(QPoint(xoffset, yoffset), mPixmap);
    painter->restore();
    ...
}

永远不要在线程中直接操作GUI控件

  • 严禁在线程中直接操作 GUI 控件
  • 必须通过 信号槽QMetaObject::invokeMethod() 切回主线程执行。

例如一个窗口,用QtConcurrent来执行一个耗时操作,操作结束后要进行界面刷新,那么可以如下执行(QtConcurrent::run就在当前界面的函数中)

1
2
3
4
5
6
7
8
9
QtConcurrent::run([this] {//this是当前窗口的指正,为了最后进行刷新
        // 执行耗时数据加载

        // 回到主线程更新模型
        QMetaObject::invokeMethod(this, [=] {
            // 更新数据
            widgetUpdate();
        });
    });

继承QObject的类,不要忘记加上Q_OBJECT宏

如果你的类继承了QObject,不要忘记加上Q_OBJECT宏,即使你没有使用信号和槽,你也要加上这个宏,否则你会遇到一些奇怪的问题

例如你需要进行多语言翻译,在需要翻译的文字使用了tr,但如果你所在的类没加上Q_OBJECT宏,你的翻译是不会生效的

自定义数据类型的信号和槽要写全命名空间

自定义数据类型,例如MyClass要作为参数在信号和槽中使用,那么必须写全命名空间,否则信号和槽无法识别

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
namespace DA{
    class MyClass{
    };
}
Q_DECLARE_METATYPE(std::shared_ptr< DA::MyClass >)

//在cpp中注册:
qRegisterMetaType< std::shared_ptr< DA::MyClass > >();

//应用此类型作为信号参数
namespace DA{
    class OtherClass{
    Q_SIGNALS:
        void mySignal(std::shared_ptr< DA::MyClass > val);
    }
}

上面代码中,由于Q_DECLARE_METATYPE必须在命名空间外定义,所以std::shared_ptr< DA::MyClass >必须写全命名空间

而作为使用这个参数的类,信号定义时,虽然在命名空间内,但信号/槽的参数必须写全命名空间,否则Qt会识别不了

注意

qRegisterMetaType建议不要传入参数,让qt自动识别
建议使用自动注册机制,参考DAGlobals.hDA_AUTO_REGISTER_META_TYPE

信号和槽的参数要匹配

所谓匹配是不要信号定义值传递,槽定义了一个常引用来接收,例如

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
namespace DA{
    class MyClass{
    };
}
Q_DECLARE_METATYPE(std::shared_ptr< DA::MyClass >)

//信号定义
namespace DA{
    class OtherClass{
    Q_SIGNALS:
        void mySignal(std::shared_ptr< DA::MyClass > val);
    }
}

//槽函数
namespace DA{
    class OtherClass2{
    Q_SLOTS:
        void mySlot(const std::shared_ptr< DA::MyClass >& val);
    }
}

上面代码中,信号和槽的参数就没有匹配,一个值传递,一个常引接收,这是不匹配的,Qt在某些情况会识别不了,这个调试起来非常困难