Qt的編程風格與規范


Qt的編程風格與規范

來源: http://blog.csdn.net/qq_35488967/article/details/70055490

參考資料:

  1. https://wiki.qt.io/Qt_Contribution_Guidelines
  2. https://wiki.qt.io/Qt_Coding_Style
  3. https://wiki.qt.io/Coding_Conventions
  4. https://community.kde.org/Policies/Library_Code_Policy
  5. https://wiki.qt.io/UI_Text_Conventions
  6. https://wiki.qt.io/API_Design_Principles
  7. http://doc.qt.io/qt-5/qml-codingconventions.html
  8. https://google.github.io/styleguide/cppguide.html

變量聲明

  • 聲明每一個變量都要用獨立的一行
  • 避免短的或無意義的命名
  • 單個字符的變量名只適用於用來計數的臨時變量,因為此時該變量的用途十分明顯
  • 當一個變量被用到時再聲明它
 // Wrong int a, b; char *c, *d; // Correct int height; int width; char *nameOfThis; char *nameOfThat;

變量一般命名法

  • 變量名和函數名以小寫字母開頭,開頭之后的部分每個單詞以大寫字母開頭
  • 避免使用縮寫
// Wrong short Cntr; char ITEM_DELIM = ' '; // Correct short counter; char itemDelimiter = ' ';

變量在Qt中的命名

  • 類名以大寫字母開頭,公開類以Q開頭,緊跟大寫字母;公用函數以q開頭。(此為Qt內部規范,我們可不遵守)
  • 首字母縮寫詞出現在命名中,采用駝峰命名法,如QXmlStreamReader,而不是QXMLStreamReader(即只有第一個字母大寫)

空白行與空格的使用

  • 用空行在適當的地方划分代碼塊
  • 總是只用一個空行
  • 在關鍵詞和花括號之間總是只用一個空格符
 // Wrong if(foo){ } // Correct if (foo) { }

指針的書寫規范

  • 對於指針或引用,在類型名和或&之間用一個空格,但是在或&和變量名之間沒有空格
 char *x; const QString &myString; const char * const y = "hello";

二元操作符

  • 二元操作符的左右都要有空格
  • 二元操作符對待它的兩個參數是同樣對待的,只是在該操作符是類外的操作符
  • 例如QLineF有它自己的==操作符
QLineF lineF;
QLine lineN;

if (lineF == lineN) // Ok, lineN is implicitly converted to QLineF if (lineN == lineF) // Error: QLineF cannot be converted implicitly to QLine, and the LHS is a member so no conversion applies

逗號

  • 逗號左邊沒有空格,逗號右邊有一個空格
  #include <QApplication> #include <QMessageBox> int main(int argc, char *argv[]) { QT_REQUIRE_VERSION(argc, argv, "4.0.2") QApplication app(argc, argv); ... return app.exec(); }

分號

  • 分號左邊沒有空格,分號作為語句的結束符,其右邊一般不再有內容
  struct Point2D { int x; int y; };

井號

  • #號右邊沒有空格
  #include <QtGlobal> #if (QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)) #include <QtWidgets> #else #include <QtGui> #endif

引號

  • 左引號的左邊和右引號的右邊都有一個空格,左引號的右邊和右引號的左邊都沒有空格
  • 如果右引號右邊是又括號的話,它們之間沒有空格
qDebug() << Q_FUNC_INFO << "was called with value1:" << value1 << "value2:" << value2; QT_REQUIRE_VERSION(argc, argv, "4.0.2")

cast

  • cast后無須空格
 // Wrong char* blockOfMemory = (char* ) malloc(data.size()); // Correct char *blockOfMemory = reinterpret_cast<char *>(malloc(data.size()));
  • 避免C語言的casts,盡量用C++的casts (static_cast, const_cast, reinterpret_cast)。 reinterpret_cast 和 C風格的cast用起來都是危險的,但至少 reinterpret_cast 不會把const修飾符去掉。
  • 涉及到QObjects或重構自己的代碼時,不要使用dynamic_cast,而是用qobject_cast,例如在引進一個類型的方法時
  • 用構造函數去cast簡單類型,例如:用int(myFloat)代替(int)myFloat

語句

  • 不要在一行寫多條語句
  • 另起一行寫控制流語句的定義
 // Wrong if (foo) bar(); // Correct if (foo) bar();

花括號寫法

  • 使用緊貼括號:左括號和語句的開頭在同一行,如果右括號是緊跟在一個關鍵詞之后的,則右括號和該關鍵詞在同一行
 // Wrong if (codec) { } else { } // Correct if (codec) { } else { }
  • 例外:函數的實現和類的聲明中,左括號總是在一行的開頭
 static void foo(int g) { qDebug("foo: %i", g); } class Moo { };
  • 當條件語句的執行部分多於一句的時候才使用花括號
 // Wrong if (address.isEmpty()) { return false; } for (int i = 0; i < 10; +''i) { qDebug("%i", i); } // Correct if (address.isEmpty()) return false; for (int i = 0; i < 10;i) qDebug("%i", i);

花括號用途

  • 例外1:如果父語句占有多行,或經過多層封裝,子語句要用到花括號
 // Correct if (address.isEmpty() || !isValid() || !codec) { return false; }
  • 例外2:對稱原則:在if-else語句塊中,如果if或else中的一個包含了多行,另一個為了對稱性原則,也要用花括號
 // Wrong if (address.isEmpty()) qDebug("empty!"); else { qDebug("%s", qPrintable(address)); it; } // Correct if (address.isEmpty()) { qDebug("empty!"); } else { qDebug("%s", qPrintable(address)); it; } // Wrong if (a) … else if (b) … // Correct if (a) { … } else { if (b) … }
  • 當條件語句的執行體是空語句的時候,用一個花括號
 // Wrong while (a); // Correct while (a) {}

圓括號

  • 圓括號用來給語句分組
 // Wrong if (a && b || c)  // Correct if ((a && b) || c)  // Wrong a + b & c  // Correct (a + b) & c

Switch 語句

  • case標簽和switch在同一列
  • 每一個case語句的末尾都要有一個break語句或return語句,除非因功能需要故意不加或另外一個case是緊跟上一個case的。
 switch (myEnum) { case Value1: doSomething(); break; case Value2: case Value3: doSomethingElse(); // fall through default: defaultHandling(); break; }

跳轉語句

  • 包括:break, continue, return, and goto
  • 不要在跳轉關鍵詞后邊加else
// Wrong if (thisOrThat) return; else somethingElse(); // Correct if (thisOrThat) return; somethingElse();
  • 例外:如果這段代碼是固有的對稱結構,用else實現視覺上的對稱也是可以的

換行

  • 每行代碼不多於100個字符;若有必要,用括號括起來
  • 逗號在行尾。操作符在新行的開頭位置,這是因為編輯器過窄的話,操作符在行尾容易看不見
  • 換行時盡量避免行於行之間看起來參差不齊
 // Wrong if (longExpression + otherLongExpression + otherOtherLongExpression) { } // Correct if (longExpression + otherLongExpression + otherOtherLongExpression) { }

一般例外與Artistic style選項

  • 一般例外:當嚴格遵守一條規范會讓你的代碼看起來很糟糕時,廢棄這條規范
  • 用astyle格式化代碼時的選項
--style=kr --indent=spaces=4 --align-pointer=name --align-reference=name --convert-tabs --attach-namespaces --max-code-length=100 --max-instatement-indent=120 --pad-header --pad-oper
  • 例如,你可以這樣用以上的代碼
 int foo = some_really_long_function_name(and_another_one_to_drive_the_point_home( first_argument, second_argument, third_arugment));

C++特性

  • 不要使用異常處理
  • 不要使用運行時類型識別
  • 理智地使用模板,不要僅僅因為你會用就去用

Qt源碼中的規范

  • 所有代碼都是ascii,使用者如果不確定的話,只可能是7字節
  • 每一個QObject的子類都必須有Q_OBJECT宏,即使這個類沒用到信號或槽。否則qobject_cast將不能使用
  • 在connect語句中,使信號和槽的參數規范化(參看 QMetaObject::normalizedSignature),可以加快信號/槽的查找速度。可以使用qtrepotools/util/normalize規范已有代碼

包含頭文件

  • 用#include
 #include <qstring.h> // Qt class #include <new> // STL stuff #include <limits.h> // system stuff
  • 如果你想包含qplatforms.h,把它作為第一個被包含的頭文件
  • 如果你想包含私有頭文件,要十分小心。使用以下的語法而不用管whatever_p.h屬於哪個模塊在哪個文件目錄下
 #include <private/whatever_p.h>

編譯器/平台特定問題

  • 使用三目運算符 ?時要特別小心,如果每次的返回值的類型可能不一樣的話,一些編譯器會在運行時生成沖突的代碼(此時編譯器甚至不會報錯)
 QString s;
 return condition ? s : "nothing"; // crash at runtime - QString vs. const char *
  • 要特別小心對齊問題。無論何時,當一個指針被cast后的對齊數是增加的時候,它都可能會崩潰。例如一個const char 被cast成了const int,當cast之后的數字不得不在2或4個字節之間對齊時,指針就會在機器上崩潰。
  • 使用一個union強迫編譯器正確地對齊變量,示例如下,你可以確定AlignHelper中所有的實例都和int邊界對齊了
 union AlignHelper { char c; int i; };
  • 任何需要需要執行構造函數或相關代碼進行初始化的實例,都不能用作庫代碼中的全局實例。因為當構造函數或代碼將要運行的時候,該實例還沒有被定義(在第一次調用該實例時,在加載庫時,在執行main()之前)
 // global scope static const QString x; // Wrong - default constructor needs to be run to initialize x static const QString y = "Hello"; // Wrong - constructor that takes a const char * has to be run QString z; // super wrong static const int i = foo(); // wrong - call time of foo() undefined, might not be called at all
  • 你可以按照下面的方法去做:
 // global scope static const char x[] = "someText"; // Works - no constructor must be run, x set at compile time static int y = 7; // Works - y will be set at compile time static MyStruct s = {1, 2, 3}; // Works - will be initialized statically, no code being run static QString *ptr = 0; // Pointers to objects are ok - no code needed to be run to initialize ptr
  • 用Q_GLOBAL_STATIC定義全局實例
 Q_GLOBAL_STATIC(QString, s)

 void foo() { s()->append("moo"); }
  • char型變量是有符號的還是無符號的取決於它運行環境的架構。如果你明確地想使用一個signed或unsinged char,就使用signed char或unsigned char。以下代碼運行在把char默認為無符號的平台上時,其條件判斷恆為真。
 char c; // c can't be negative if it is unsigned /********/ /*******/ if (c > 0) { … } // WRONG - condition is always true on platforms where the default is unsigned
  • 避免64位的枚舉值 
    • 嵌入式應用系統二進制接口將所有的枚舉類型的值硬編碼成32位int值
    • 微軟的編譯器不支持64位的枚舉值

編程美學

  • 偏愛用枚舉值定義常量而非用const int或defines 
    • 枚舉值會在編譯時被編譯器用實際值替換掉,因而運行時得出結果的速度更快
    • defines不是命名空間安全的(並且看起來很丑)
  • 偏愛使用冗長而詳細的參數名
  • 當重新實現一個虛方法時,不要在頭文件中用virtual關鍵字,在Qt5中,用 Q_DECL_OVERRIDE宏在函數聲明之后,分號之前注解它。

避免出現的事

  • 不要繼承模版/工具類 
    • 其析構函數不是虛函數,會導致潛在的內存泄漏
    • 其符號不是導出的(not exported),會導致符號沖突
// 例如:A庫有以下代碼 class Q_EXPORT X: public QList<QVariant> {};
//B庫有以下代碼 class Q_EXPORT Y: public QList<QVariant> {};

這樣,QList的符號就被導出了兩次

  • 不要把const-iterator和none-const iterator搞混。
 for (Container::const_iterator it = c.begin(); it != c.end(); ++it) // W R O N G for (Container::const_iterator it = c.cbegin(); it != c.cend(); ++it) // Right
  • Q[Core]Application 是一個單例類。同一時間只能有一個實例在運行,但是這個實例可以被銷毀,新的實例將可以被創建,如下的代碼容易產生崩潰
 static QObject *obj = 0; if (!obj) obj = new QObject(QCoreApplication::instance());
  • 當QCoreApplication application被銷毀時,obj成為了迷途指針(野指針),可以用 Q_GLOBAL_STATIC 和qAddPostRoutine清理
  • 為了盡可能地支持靜態關鍵詞,避免使用匿名命名空間。編譯單位內的一個靜態名稱可以保證它是一個內部連接。而一個位於匿名命名空間的名稱,C++規定它是一個外部鏈接。

二進制和源兼容性

  • 定義 
    • Qt 4.0.0是一個主版本,Qt 4.1.0是一個微調版本,Qt 4.1.1是一個補丁版本。
    • 在此之后的版本:代碼鏈接到之前版本的庫可以運行
    • 在此之前的版本:代碼鏈接到一個新版本的庫只對舊版本的庫能工作。
    • 源碼兼容性:源碼在不修改的情況下進行編譯
  • 微調版本保持向后的二進制兼容性
  • 補丁版本保持向后和向前的二進制兼容性 
    • 不要增加或去掉任何公共API(例如公共的函數,公有/保護/私有的方法)
    • 不要重新實現方法(甚至不要修改內連方法,也不要修改保護/私有方法)
  • 當繼承一個QWidget的子類時,總是要去重寫event(),即使它是空的。這將使你的widget類可以被操作而不破壞其二進制兼容性
  • 所有從Qt中導出的方法,必須以q或Q開頭。用autotest符號檢測是否存在違反此規則的情況。(此為Qt本身要求的規范,我們不需要嚴格執行。)

命名空間

  • 請記住,Qt中,除了Tests和WibKit,全部都是處在命名空間中的代碼

float值

  • 沒有float值之間的比較 
    • 用qFuzzyCompare去和delta比較其值
    • 用qIsNull去判斷float值是不是二進制0,而不是和0.0比較。
[static] bool qFuzzyCompare(double p1, double p2) // Instead of comparing with 0.0 qFuzzyCompare(0.0,1.0e-200); // This will return false // Compare adding 1 to both values will fix the problem qFuzzyCompare(1 + 0.0, 1 + 1.0e-200); // This will return true

虛方法

  • 不要在子類中隱藏父類的虛方法:假設A類中有個virtual int val()方法,以下代碼是不規范的。
 class B: public A { using A::val; int val(int x); };

宏定義

  • 在操作一個預處理器之前,先判斷它是否被定義
 #if Foo == 0 // W R O N G #if defined(Foo) && (Foo == 0) // Right #if Foo - 0 == 0 // Clever, are we? Use the one above instead, for better readability

類的成員命名

  • 成員變量一般為名詞
  • 函數成員一般為動詞/動詞+名詞,但是當動詞為get時,get常常省略。當返回值為Bool型變量時,函數名一般以前綴’is’開頭
public: void setColor(const QColor& c); QColor color() const; void setDirty(bool b); bool isDirty() const; private Q_SLOTS: void slotParentChanged();

定義私有類

//.h文件 class KFooPrivate; class KFoo { public: /* public members */ private: const QScopedPointer<KFooPrivate> d; };
//.cpp文件
class KFooPrivate
{
    public:
        int someInteger;
};

KFoo::KFoo() : d(new KFooPrivate)
{
    /* ... */ } KFoo::~KFoo() { // You must define a non-inline destructor in the .cpp file, even if it is empty // else, a default one will be built in placed where KFooPrivate is only forward // declare, leading to error in the destructor of QScopedPointer }

標記(flags)

  • 避免使用無意義的bool型參數,以下是糟糕的例子
static QString KApplication::makeStdCaption( const QString &caption, bool withAppName, bool modified);
  • 解決方案是用QFlags。即使其中只有一個值,也建議這么做,這將允許你以后很方便地添加更多的值並且保持二進制兼容性。
  • 示例如下:
class KApplication
{
public: /* [...] */ enum StandardCaptionOption { /** * Indicates to include the application name */ WithApplicationName = 0x01, /** * Note in the caption that there is unsaved data */ Modified = 0x02 }; Q_DECLARE_FLAGS(StandardCaptionOptions, StandardCaptionOption) /** * Builds a caption using a standard layout. * * @param userCaption The caption string you want to display * @param options a set of flags from MakeStandartCaptionOption */ static QString makeStandardCaption(const QString& userCaption, const StandardCaptionOptions& options = WithApplicationName); /* [...] */ }; Q_DECLARE_OPERATORS_FOR_FLAGS(KApplication::StandardCaptionOptions)

常引用

  • 每一個對象,只要它不是基礎類型(int, float, bool, enum, or pointers),都應該以常量引用的形式傳遞。這條使得代碼運行得更快。即使一個對象是隱式共享的,也應該這么做
QString myMethod( const QString& foo, const QPixmap& bar, int number );
  • 避免常引用的返回類型
const QList<int> &someProperty() const;
  • 有種情況還是可以使用常引用的返回類型的,這種情況下,此處代碼的運行性能至關重要,此處的代碼實現也是固定的,思考再三之后,你可以寫成這樣:
QList<int> someProperty() const;

庫代碼中的信號&槽

  • 在庫代碼中,用Q_SIGNALS 和 Q_SLOTS 代替 signals 和 slots。它們在語法上是相等的,用以避免和boost信號的沖突。和python協同工作時使用”slots”

屬性

  • 設置屬性時用Q_PROPERTY。理由是屬性可以被javascript 接口訪問到
  • moc中設置特定的flag用 QMetaProperty.

構造函數

  • 為了使構造函數被錯誤使用的可能性降到最小,每一個構造函數(除了拷貝構函數)都應該檢查自己是否需要加上explicit 符號。

#include

  • 盡量減少在頭文件中包含其他頭文件的數量
  • 如下所示,可以用前置聲明法
#include <kfoobase.h> class KBar; class KFoo : public KFooBase { public: /* [...] */ void myMethod(const KBar &bar); };
  • 包含Qt自帶頭文件或外部庫的頭文件用尖括號
#include <iostream> #include <QDate> #include <zlib.h>
  • 包含自己的項目頭文件用雙引號
#include "myclass.h"
  • 包含Qt類的頭文件不用包含它所在的模塊名
//正確示例 #include <QDate> //correct
//錯誤示例 #include <QtCore/QDate> //incorrect, no need to specify the module QtCore #include <QtCore> //incorrect, do not include the top-level Qt module
  • 假如你有一個Foo類,有Foo.h文件和Foo.cpp文件,在你的Foo.cpp文件中,要先包含Foo.h文件再包含其他頭文件
  • 如果你的代碼寫成下面這樣:
//.h文件 class Foo { public: Bar getBar(); };
.cpp文件
#include "bar.h" #include "foo.h"
  • 你的cpp文件能夠正常編譯,但當其他人用到Foo.h文件時,如果沒有包含bar.h文件,編譯器將不能進行編譯。
  • 因此在cpp文件中首先包含其相應的.h文件,是為了.h文件能夠被其他人使用
  • 頭文件必須進行包含保護:避免多次包含而引起多次的編譯
#ifndef MYFOO_H #define MYFOO_H ... <stuff>... #endif /* MYFOO_H */

信號&槽的標准化寫法

  • 標准化的寫法增加代碼可讀性
  • 不標准的寫法可能是如下寫法
QObject::connect(this, SIGNAL( newValue(const QString&, const MyNamespace::Type&) ), other, SLOT( value(const QString &) ));
  • 建議采用以下寫法
QObject::connect(this, SIGNAL(newValue(QString,MyNamespace::Type)), other, SLOT(value(QString)));

API-最小化原則

  • 最小化的API意味着,每個API中使用盡可能少的類,每個類中使用盡可能少的公用成員(public members)。這樣做的好處是使得API易於理解、記憶、調試和修改

API-完整性原則

  • 一個完整的API意味着實現它預期的功能。這和最小化的特性可能會產生沖突。另外如果一個成員函數出現在一個錯誤的類里,API的使用者們可能會找不到它

API-有明確和簡單的語意

  • 正如其他的設計工作一樣,你應當遵守“最小驚奇”原則。一般情況下,這是容易做到的。請不要用解決方案所解決的問題過於籠統。(例如Qt3中的QMimeSourceFactory,應該被叫做 QImageLoader從而作為一個不同的API)

API-直觀性原則

  • 不同的經歷和背景讓人們對什么具有“直觀性”什么不具有,有着不同的感覺。以下情況我們可以認為一個API是直觀的
  • 一個稍有一些經驗的用戶在不看幫助文檔的情況下,能夠正確地使用該API
  • 一個從不知道該API的用戶能夠看懂使用該API寫成的代碼

API-便於記住

  • 選擇一個一致的和准確的命名約定
  • 使用可識別的模式和概念
  • 避免使用縮寫

API-易讀性原則

  • 代碼是一次寫成,但需要多次閱讀,易讀的代碼可能寫的時候會花費稍長的時間,但是在整個產品的生命周期之中,但節省你很多閱讀和理解的時間
  • 最后,記住不同的用戶會用到一個API的不同部分。雖然直接使用Qt的類生成一個實例是直觀的,但我們還是有理由期待用戶在派生一個Qt的類之前先閱讀它的官方文檔

API-靜態多態性

  • 相似的類應該有相似的API,這可以用繼承的方式實現,這用到了運行時多態。
  • 但是多態也可以發生在設計類的時候,例如,你把一個對象的類型從QProgressBar 換成Qslider,或者從QString 換成QByteArray,你會發現它們之間的API是何其的相似,以至於你可以很簡單地用一個去替換另一個。這就是我們所說的“靜態多態性”
  • “靜態多態性”使得記住這些API和使用編程模式都更為簡單了。因此,為一系列相關類設計相似的API好過為每個類設計獨立的、更切合自身的API
  • 一般來說,在Qt中我們偏愛使用“靜態多態性”而非實際的繼承,除非一些不可控制的原因要求我們不得不如此。

常引用

  • 如果一個類型超過16個字節,用常引用傳遞它。
  • 如果一個類型有一個非平凡拷貝構造函數(non-trivial copy-constructor),或一個非平凡析構函數(non-trivial destructor),用常引用傳遞它的值而避免使用以上方法
  • 所有的其他類型都應該直接傳遞其值
void setAge(int age); void setCategory(QChar cat); void setName(QLatin1String name); void setAlarm(const QSharedPointer<Alarm> &alarm); // const-ref is much faster than running copy-constructor and destructor // QDate, QTime, QPoint, QPointF, QSize, QSizeF, QRect are good examples of other classes you should pass by value.

枚舉類型和枚舉值的命名

  • 以下的示例說明了枚舉值命名時給出一般的名稱的危險
namespace Qt
{
enum Corner { TopLeft, BottomRight, … }; enum CaseSensitivity { Insensitive, Sensitive }; … };
tabWidget->setCornerWidget(widget, Qt::TopLeft); str.indexOf("$(QTDIR)", Qt::Insensitive);
  • 在最后一行,Insensitive 是什么含義呢?這是不易於理解的。因此,枚舉值命名時,至少重復枚舉類型名中的一個字母
namespace Qt
{
enum Corner { TopLeftCorner, BottomRightCorner, … }; enum CaseSensitivity { CaseInsensitive, CaseSensitive }; … };
tabWidget->setCornerWidget(widget, Qt::TopLeftCorner); str.indexOf("$(QTDIR)", Qt::CaseInsensitive);

企圖少寫代碼的陷阱

  • 不要為了圖方便少些一些代碼。因為代碼是一次書寫,后期不止一次地要去理解。例如
QSlider *slider = new QSlider(12, 18, 3, 13, Qt::Vertical, 0, "volume");
  • 改成下面的方式會更容易理解
QSlider *slider = new QSlider(Qt::Vertical); slider->setRange(12, 18); slider->setPageStep(3); slider->setValue(13); slider->setObjectName("volume");

Bool型參數陷阱

  • 這方面經典的例子是Qt中的repaint()函數,它的bool型參數用來標記窗口的背景是否被擦除。用法如下:
widget->repaint(false);
  • 上面的代碼很容易被理解成“不重新繪制”
  • 上面代碼的中用到的repaint()函數,其設計時的考慮無非是為了可以少定義一個函數,結果反而帶來了誤解,有多少人可以對以下三行代碼所代表隊含義進行准確地區分呢?
widget->repaint(); widget->repaint(true); widget->repaint(false);
  • 稍微好一點的做法是這樣
widget->repaint(); widget->repaintWithoutErasing();
  • 還有一個很明顯的做法是盡可能地用枚舉類型代替bool型參數,請對比以下的兩行代碼
str.replace("USER", user, false); // Qt 3 str.replace("USER", user, Qt::CaseInsensitive); // Qt 4

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM