درود
این جلسه میخوایم درباره فریم ورک Qt صحبت کنیم و چندتا از کامپوننت های Qt رو استفاده کنیم.
این ترم شما با برنامه نویسی cpp و سینتکس اون آشنا شدین و میتونید ازش استفاده کنید تا برنامه هایی رو پیاده سازی کنید، ولی آیا الان میتونید از صفر یه بازی گرافیکی بنویسید؟
برای همچین کاری نیاز به دانشی بیشتر از دانش برنامه نویسی یک زبان دارید، باید با سیستم عامل و امکانات و طرز کارش آشنا باشید.
برای خیلی از کارها نیاز هست تا کد هایی بر اساس سیستم عامل پیاده سازی بشه و خب ما این ترم نمیخوایم زیاد وارد این موارد بشیم و میخوایم از یک مجموعه ابزار که بهمون قابلیت هایی رو میدن که میتونیم محیط گرافیکی ایجاد کنیم، از طریق اینترنت به جایی درخواست بفرستیم و جواب بگیریم و یا کیبورد و موس رو کنترل بکنیم.
اسم این ابزار Qt (کیوت یا کیوتی) هست، برای نصب این ابزار میتونید به آدرس زیر مراجعه کنید و نسخه Open Source یا همون Community اون رو دانلود و نصب کنید (لازم نیست نسخه پولی اون رو دانلود کنید یا فرم تریال پر کنید، شما نسخه کاربران متنباز(for open source users) رو باید دانلود کنید)
زمان نصب گزینه custom installation رو بزنید و مورد Qt 6.4.2 رو گزینه های داخل عکس رو انتخاب کنید، این موارد برای کارهایی که میخوایم انجام بدیم کفایت میکنه.
نسخه 6.4.2 نسخه LTS(long term support) یا پایدار Qt میباشد و اینکه ما از کتابخونه QtMultimedia برای کار با فایل های صوتی استفاده میکنیم.
وقتی Qt رو نصب میکنید ابزاری به اسم Qt Creator روی سیستمتون نصب میشه که عملا IDEه Qt هست، ما یه کم روی IDE کار میکنیم و بعد میریم روی CLion (علتش هم اینه که توسعه راحت تر هست و هوشمندی CLion بیشتر از Qt Creator هست).
بریم یک پروژه روی Qt Creator ایجاد کنیم، برای اینکار روی Create Project در صفحه Welcome کلیک میکنیم یا از طریق منوی File اقدام میکنیم که با پنجره زیر رو به رو میشیم
برای شروع ما میخوایم یک برنامه تحت کامند لاین توسط Qt توسعه بدیم، بعد از این مرحله نام پروژه رو باید انتخاب کنیم و در مرحله بعد باید Build System رو انتخاب کنیم
ما میتونیم از سه نوع Build System استفاده کنیم
1. qmake که دیگه توسعه داده نمیشه
2. cmake که گزینه پیشنهادی برای Build System هست
3. qbs که عملا نسخه جدید qmake هست که در حال توسعه هست
ابزار های دیگه ای هم برای اینکار موجود هست، در واقع Build System ابزاری هست که ما از اون برای مدیریت پروژه و بیلد کردن اون پروژه ازش استفاده میکنیم.
مرحله بعد برامون سیستمی رو محیا میکنه تا بتونیم جملات و کلماتی که در برنامه قراره به کاربر نشون بدیم رو در فایلی ذخیره کنیم، این ویژگی در زمان چند زبانه بودن برنامه بسیار کاربردی هست، توجه داشته باشید که استفاده از این قابلیت میتونه به خوانا تر شدن و مرتب تر بودن کدتون نیز کمک کنه.
مرحله بعد Development Kit یا همون نسخه Qt ای که میخواید باهاش پروژه رو بیلد بگیرید انتخاب میکنید
در نهایت هم ورژن کنترل یا همون Git رو انتخاب میکنید تا بتونید بهتر مدیریت روی تغییراتتون داشته باشید.
توجه داشته باشید که میتونید برای ایجاد پروژه از همون IDEه CLion نیز استفاده کنید.
برای شروع میریم سراغ یکی از پر کاربردترین کلاس ها در Qt به اسم QObject، این کلاس تقریبا در خیلی از کلاس های Qt استفاده میشه و ویژگی هایی رو برای یک کلاس محیا میکنه، حالا بریم یک مثال بزنیم، یک کلاس که از QObject ارثبری کنه میسازیم، برای اینکار فعلا بهتره از طریق Qt Creator اقدام کنیم و روی پروژه راست کلیک کنیم و گزینه Add New... رو انتخاب کنیم و در پنجره باز شده C++ class و انتخاب کنیم و از قسمت Base Class کلاس QObject رو انتخاب کنیم
همونطور که در تصویر میبینید گزینه هایی مثل Include کردن و اضافه کردن Q_OBJECT میبینید، مورد اول که واضع هست، کلاس QObject رو Include میکنه تا بتونیم ارثبری کنیم، مورد دوم یک Macro هست که قابلیت meta-object رو برای کلاس مورد نظر فعال میکنه (در مورد meta-object در اینجا میتونید مطالعه کنید)
بعد از زدن روی Finish دیالوگی نمایش داده میشه که میگه Qt توانایی اضافه کردن کلاس ها به CMake رو نداره و اطلاعات داخل clipboard شما ذخیره شده فقط کافیه در قسمت add_executable در فایل CMake که باز میشه Paste (Ctrl + V) کنید. (این عملیات در IDEه CLion بهتر عمل میکنه)
character.h
#ifndef CHARACTER_H #define CHARACTER_H #include <QObject> class Character : public QObject { Q_OBJECT public: explicit Character(QObject *parent = nullptr); signals: }; #endif // CHARACTER_H
در کد رو به رو میبینیم که ماکرو Q_OBJECT اضافه شده و از QObject ارثبری شده
حالا میخوایم به دوتا از قابلیت هایی که QObject در اختیارمون میاره بپردازیم:
1. destructor of a parent object destroys all child objects
2. signal & slot
3. غیر قابل کپی بودن
قبل از اینکه بخوام درباره قابلیت های QObject صحبت کنیم، بهتره بدونیم که Qt خیلی از کلاس های استاندارد cpp رو بازنویسی کرده و متد های کاربردی ای رو برامون محیا کرده، برای مثال برای اینکه خروجی کارمون رو تحت Console ببینیم عموما از cout داخل namespaceعه std استفاده میکنیم و برای هر خط endl میزاریم، در حالی که Qt متد هایی رو برای دیباگ و نمایش در کنسول برامون مهیا کرده که میتونه خروجی های بهتری رو برامون داشته باشه.
برای مثال به نمونه کد زیر توجه کنید
#include <QDebug> #include "character.h" int main(int argc, char *argv[]) { Character characterObj; characterObj.setObjectName("TheObjectName"); qInfo() << "This is a QObject class ->" << &characterObj; }
در این کد ما یک شی از کلاس Character ساختیم و یک نام برای این شی قرار دادیم توسط تابع setObjectName و توسط qInfo از QDebug خروجی رو به نمایش در آوردیم.
خروجی کد بالا به صورت زیر میشه
This is a QObject class -> Character(0x250f1ff6a0, name = "TheObjectName")
اگر از cout به جای qInfo استفاده میکردیم خروجی به صورت زیر میشد
This is a QObject class ->0x6d16fff790
برای شروع بهتره با یکی از کلاس های اصلی Qt آشنا بشیم، در Qt کلاسی وجود داره به اسم QObject که بسیاری از کلاس های Qt از این کلاس ارثبری کردند و قابلیت هایی رو برای ما محیا میکنه.
حالا برگردیم سراغ قابلیت های QObject
به constructor کلاسی که ساختیم دقت کنید، میبینید که به عنوان ورودی میتونید یک QObject پدر پاس بدید، ساختار QObject ها میتونه به صورت یک درخت باشه که بعد از از بین رفتن QObject پدر تمام QObject های فرزند نیز از بین میروند.
بیاید برای کلاس بالا یک destructor بنویسیم و کنترل کنیم که کی شی ساخته میشه و کی از بین میره
پس بالا رو به صورت زیر تغییر میدیم
character.h
#ifndef CHARACTER_H #define CHARACTER_H #include <QObject> class Character : public QObject { Q_OBJECT public: explicit Character(QObject *parent = nullptr); ~Character(); }; #endif // CHARACTER_H
character.cpp
#include <QDebug> #include "character.h" Character::Character(QObject *parent) : QObject{parent} { qInfo() << this << " Created" } Character::~Character() { qInfo() << this << " Destroyed" }
main.cpp
#include <QDebug> #include "character.h" #include <QCoreApplication> int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); auto parent = new Character(); auto child1 = new Character(parent); auto child2 = new Character(parent); auto child1Child1 = new Character(child1); auto child1Child2 = new Character(child1); parent->setObjectName("parent"); child1->setObjectName("child1"); child2->setObjectName("child2"); child1Child1->setObjectName("child1Child1"); child1Child2->setObjectName("child1Child2"); delete parent; qInfo() << "Object deleted!" return a.exec(); }
کاری که کردیم این بود که یک destructor برای کلاس درست کردیم و ساخت و از بین رفتن شی رو در کنسول نمایش دادیم.
خروجی کد بالا به صورت زیر هست
Character(0x1ed03bf3ec0) Created Character(0x1ed03bfd9a0) Created Character(0x1ed03bffc40) Created Character(0x1ed03bffed0) Created Character(0x1ed03c00430) Created Character(0x1ed03bf3ec0, name = "parent") Destroyed Character(0x1ed03bfd9a0, name = "child1") Destroyed Character(0x1ed03bffed0, name = "child1Child1") Destroyed Character(0x1ed03c00430, name = "child1Child2") Destroyed Character(0x1ed03bffc40, name = "child2") Destroyed Object deleted!
اگر به جای delete parent بیایم و child1 رو delete کنیم خروجی به صورت زیر میشه
Character(0x1ed03bf3ec0) Created Character(0x1ed03bfd9a0) Created Character(0x1ed03bffc40) Created Character(0x1ed03bffed0) Created Character(0x1ed03c00430) Created Character(0x1ed03bfd9a0, name = "child1") Destroyed Character(0x1ed03bffed0, name = "child1Child1") Destroyed Character(0x1ed03c00430, name = "child1Child2") Destroyed Object deleted!
بریم سراغ قابلیت دوم QObject
فرض کنید قرار هست بازی ای بنویسید که وقتی روی شخصیت های اون کلید میکنیم توابعی صدا زده بشه تا عملیاتی صورت بگیره، به شکل زیر توجه کنید
رخداد کلیک کردن از سیستم عامل به بازی شما میرسه و پس از اون اگر در اون موقعیت شخصیتی وجود داشته باشه به شخصیت میرسه، حال ما میخواهیم اگر این اتفاق افتاد توابعی که در Game پیاده کرده ایم مثل Start Game یا Remove Character صدا زده شود.
در این سناریو ما باید یک تابع Signal (مستطیل قرمز) و چندین تابع Slot(مستطیل سبز) تعریف کنیم و Signal و Slot ها رو به هم connect کنیم. برای شروع یک تابع signal داخل کلاس Character که بالا ایجاد کردیم درست میکنیم به صورت زیر
character.h
#ifndef CHARACTER_H #define CHARACTER_H #include <QObject> class Character : public QObject { Q_OBJECT public: explicit Character(QObject *parent = nullptr); ~Character(); void click(); signals: void onCharacterClicked(int characterId); }; #endif // CHARACTER_H
character.cpp
#include <QDebug> #include "character.h" Character::Character(QObject *parent) : QObject{parent} { qInfo() << this << " Created" } Character::~Character() { qInfo() << this << " Destroyed" } void Character::click() { emit this->onCharacterClicked(50); }
در کلاس بالا اومدیم یک signal رو define کردیم و یک تابع click هم تعریف کردیم که داخلش میگیم اگر صدا زده شد signalه رو emit کن(یعنی منتشر کن این signal رو).
بریم سراغ کلاس Game
game.h
#ifndef GAME_H #define GAME_H #include <QObject> class Game : public QObject { Q_OBJECT public: Game(); public slots: void EndGame(); void StartGame(); void RestartGame(); void RemoveCharacter(int characterId); }; #endif //GAME_H
game.cpp
#include "game.h" #include <QDebug> Game::Game() : QObject{} { qInfo() << this << " Created" } void Game::EndGame() { qInfo() << "game end!" } void Game::StartGame() { qInfo() << "game start!" } void Game::RestartGame() { qInfo() << "game restart!" } void Game::RemoveCharacter(int characterId) { qInfo() << "Character " << characterId << " removed from game!" }
در این کلاس هم یک سری تابع slot تعریف میکنیم.
بریم کدی برای ارتباط این دو بنویسیم
main.cpp
#include "game.h" #include "character.h" #include <QCoreApplication> int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); auto game = new Game(); auto character = new Character(); QObject::connect(character, &Character::onCharacterClicked, game, &Game::RemoveCharacter); character->click(); return a.exec(); }
در این کد قصد داریم تا صدا زدن تابع click در character تابع RemoveCharacter در game صدا زده بشه که برای اینکار از تابع connect استفاده کردیم و signal داخل character را به slot داخل game متصل کردیم.
خروجی کد بالا به صورت زیر است
Game(0x295d7c83f00) Created Character(0x295d7c8da50) Created Character 50 removed from game!
برای درک بهتر شاید تصویر زیر نیز بهتون کمک کنه
نکته نهایی در مورد QObject ها اینکه ما نمیتونیم از QObject ها کپی تهیه کنیم علتش هم به خاطر ساختار پیچیده ای هست که این نوع کلاس داره و همچنین ممکنه Signal و Slot ها بعد از کپی درد سر ایجاد کنند برای همین در جا هایی مثل کد زیر به خطا میخورد که اعلام میکنه constructor کپی از delete شده
#include "character.h" #include <QCoreApplication> void test(Character character){ } int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); auto character = new Character(); test(*character); // <--------- 'QObject' copy constructor has been explicitly marked deleted return a.exec(); }
برای حل مشکل بالا میتونید آدرس Character رو پاس بدید به صورت زیر
#include "character.h" #include <QCoreApplication> void test(Character *character){ } int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); auto character = new Character(); test(character); return a.exec(); }
شاید یکی از مسائلی که توی زبان های دیگه دیده باشید و داخل cpp یه کم براتون اذیت کننده باشه وجود کلاس List هست، موجودیتی که بتونید بدون مشخص بودن سایز از پیش تعیین شده داده هاتون رو در لیستی ذخیره کنید و یا بتونید یک ایتم از لیست رو به راحتی پیدا کنید.
اینجا ما میتونیم از کلاس QList استفاده کنیم که بهمون این امکان رو میده. فرض کنید در مثال قبل قصد داریم در کلاس Game لیستی از Character ها داشته باشیم و متدی برای اضافه و کم کردن و پیدا کردن Character ها داشته باشیم
به نمونه کد زیر توجه کنید
game.h
#ifndef GAME_H #define GAME_H #include <QObject> #include "character.h" class Game : public QObject { Q_OBJECT private: QList<Character*> characters{}; public: Game(); void RemoveCharacterAt(int position); void AddCharacter(Character *character); Character *FindCharacter(Character *character); public slots: void EndGame(); void StartGame(); void RestartGame(); void RemoveCharacterGame(int characterId); }; #endif //GAME_H
سه تابع به کلاس اضافه کردیم و یک فیلد که لیستی از character ها رو شامل میشه اضافه کردیم
game.cpp
#include "game.h" #include <QDebug> Game::Game() : QObject{} { qInfo() << this << " Created" } void Game::EndGame() { qInfo() << "game end!" } void Game::StartGame() { qInfo() << "game start!" } void Game::RestartGame() { qInfo() << "game restart!" } void Game::RemoveCharacterGame(int characterId) { qInfo() << "Character " << characterId << " removed from game!" } void Game::AddCharacter(Character* character) { this->characters.append(character); } void Game::RemoveCharacterAt(int position){ this->characters.removeAt(position); } Character* Game::FindCharacter(Character* character) { return this->characters.at(this->characters.indexOf(character)); }
همون طور که مشاهده میکنید اینجا میتونیم از متد هایی مثل append و remoteAt و at و بسیاری متد دیگه استفاده کنیم
همونطور که اول جلسه اشاره کردم Qt سعی کرده در هر زمینه ای کلاس و متد هایی رو برای راحتی در cpp برای برنامه نویس ها محیا کنه، برای مثال میتونیم از کلاس هایی که برای کار با فایل پیاده شده استفاده کنیم
به کد زیر توجه کنید
#include <QFile> #include <QDir> int main(int argc, char *argv[]) { QFile file{"test.txt"}; if(file.open(QIODevice::ReadWrite)) { file.write("Hello World!"); file.flush(); file.write("\r\nAdd Some Text..."); file.flush(); file.seek(6); file.write(" Reza"); file.copy("test_copy.txt"); file.remove(); } QDir dir{"."}; for (const auto &item: dir.entryList(QDir::Filter::Files)) qInfo() << item; return 0; }
در کد بالا یک شی از کلاس QFile درست کردیم و برای خواندن و نوشتن فایل رو باز کردیم، داخل فایل عبارت Hello World! رو نوشتیم و flush کردیم تا اطلاعات بر روی دیسک ذخیره شود(تا زمانی که فایل flush یا close نشه اطلاعات بر روی هارد نوشته نمیشه) بعد از اون یک عبارت دیگه داخل فایل مینویسیم و به موقعیت کاراکتر 6 ام میرویم و کلمه Reza رو مینویسیم و یک کپی از فایل درست میکنیم به اسم test_copy.txt و فایل قبلی رو حذف میکنیم.
بعد از اون میخوایم لیست فایل های درون دایرکتوری کنونی رو بدست بیاریم و در کنسول نمایش بدهیم که از تابع entryList استفاده میکنیم و فیلتر روی Files میزاریم.