גרף האוביקטים ו QObject

מחלקת הבסיס ב Qt נקראת QObject. בשעור זה נלמד מהי ואיזה פונקציונאליות היא מוסיפה לתוכניות שלנו.

1. הלולאה הראשית - Qt Event Loop

אחת הבעיות הראשונות שעולות כאשר אנו לוקחים שפת תכנות קיימת, במקרה שלנו C++, ובונים עבורה ממשק משתמש גרפי היא נושא האירועים. הפרדיגמה של טיפול באירועים שונה מאוד מפרדיגמת התכנות הרגילה, מאחר ותוכנית אינה רצה מתחילתה עד סופה, אלא שבכל פעם ש"משהו קורה" יש לבצע קוד מסוים.

מערכות שהן Event Driven, כולל סביבות GUI שמתנהגות כך, מבוססות בדרך כלל על מנגנון של MainLoop. הרעיון הוא יחסית פשוט, יש לנו תהליך ביצוע ראשי של התוכנית המבצע את הפעולות הבאות בלולאה:

  1. המתן שמשהו יקרה
  2. טפל באירוע שקרה על ידי הפעלת קוד שנרשם מראש בתור Event Handler.

מנגנון זה אינו יחודי ל Qt, וגם ב Qt הוא מוטמע עמוק מאוד בפריימוורק. לכל אובייקט ב Qt יש אפשרות להגדיר באיזה אירועים הוא מטפל, ואיזה אירועים הוא משגר (שימו לב, שב Qt יש שתי שכבות של טיפול באירועים - ה Signals & Slots ו QEvent. נדבר על שתיהן ועל ההבדלים ביניהן). כזכור הפקודה exec גורמת לכניסת היישום ללולאה הראשית.

הרעיון של QObject כאובייקט אב הנותן אקסטרה פונקציונאליות לכל אובייקט במערכת, ומאפשר לכל אובייקט לקחת חלק ב Event Loop, ליווה את Qt כבר מהגירסא הראשונה, ובנוסף לתפקידו בניהול האירועים הוא מאפשר גם מנגנון ייחודי ל Qt של ניהול זכרון.

לכן ל QObject שני תפקידים מרכזיים:

  1. ניהול בעלות: בממשק גרפי רכיבים מסוימים שייכים למסך מסוים. המחלקה QObject כוללת קוד שמאפשר הגדרת שייכות כזו, כך שכשהמסך נמחק מהזכרון גם כל הרכיבים הקשורים אליו נמחקים.

  2. חיבור לאירועים: המחלקה QObject כוללת קוד המאפשר לכל QObject לקבל ולטפל באירועים שמגיעים מה Event Loop. מבחינה זו כל מנגנון שפועל בצורה אסינכרונית ב Qt יירש מ QObject, גם אם אינו חלק מממשק המשתמש. לדוגמא חיבור רשת יכול לרשת מ QObject ולקבל אירועים כשמידע חדש יגיע דרך הרשת.

2. ניהול בעלות

לצורך הקלת עבודת ניהול הזכרון ב Qt, אובייקטים ב Qt מסודרים בעצי אובייקטים ביחסים המכונים parent-child. שימו לב שזה אינו קשור להורשה של מחלקות כלל. הורשה של מחלקות מתיחסת לשיתוף פונקציונאליות בין מחלקות שונות, ואילו כאן מדובר על יחס של בעלות בין אוביקטים. הדבר נועד למצבים בהם מסך מסוים ביישום מחזיק בעלות על הרכיבים שצריך כדי להציג אותו, וכך כשהמסך נמחק מהזכרון גם כל הרכיבים שלו ימחקו.

ביצירת QObject חדש אנו יכולים לתת לו בבנאי QObject אחד שיהיה ה parent שלו. Qt יקרא באופן אוטומטי ל setParent ויוסיף אובייקט זה למערך הילדים של האבא. כעת, כאשר אובייקט נמחק - אוטומטית הוא מוחק גם את כל הילדים שלו.

נתבונן בדוגמא הבאה לקוד בקובץ main.cpp:

#include <QtWidgets>
#include <QtGui>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QWidget w;

    QVBoxLayout *layout = new QVBoxLayout(&w);
    QLabel *l           = new QLabel("Ouch!", &w);
    QPushButton *b      = new QPushButton("Click Me", &w);

    layout->addWidget(l);
    layout->addWidget(b);

    w.show();

    return a.exec();
}

חלק מהרכיבים בתוכנית אמורים להיות כבר מוכרים לכם: יצירת QApplication בהתחלה והפעלת exec בסוף, יצירת כפתורים ותיבות טקסט והצגת פקד. בנוסף יש כאן שני דברים שעדיין לא ראינו:

  1. המחלקה QWidget היא מחלקת הבסיס לפקד ויזואלי כלשהו. היא מייצגת פקד ריק ומשתמשים בה כלוח לשים עליו פקדים כשרוצים לשלב מספר פקדים בחלון אחד.

  2. מנגנון ה Layout עליו נדבר באחד השעורים הבאים, הוא שמאפשר את סידור הכפתור ותיבת הטקסט אחד מתחת לשני.

אבל מה שבאמת מעניין כאן זה היעדר הקריאות ל delete משלושת האוביקטים שיצרנו עם new. זו לא טעות: אוביקטים אלו הם חלק מהמסך ולכן הבנאי שלהם מקבל את הפקד הראשי בתור הפרמטר האחרון. העברה זו הופכת את w לאב של שלושתם, וכך כש w יימחק ביציאה מהפונקציה (בתור משתנה אוטומטי) יימחקו אוטומטית גם שלושת האוביקטים שהוא אחראי עליהם.

3. פונקציות הקשורות לעץ האוביקטים

ל QObject יש מספר פונקציות הקשורות לעץ האוביקטים:

  1. הפונקציה QObject::children מחזירה את רשימת הילדים של אוביקט מסוים. בתוכנית הדוגמא שלנו נוכל להוסיף את שורת ההדפסה הבאה לקוד כדי להיווכח שאכן לאוביקט המסך יש 3 ילדים:
qDebug() << "Widget w has: " << w.children().size() << " children";
  1. הפונקציה QObject::findChildren משמשת לאיתור ילדים לפי שם המחלקה. מעבירים לה את שם המחלקה כמחרוזת ומקבלים רשימה של כל הילדים מסוג מסוים. אפשרות יותר מקובלת היא להשתמש ב Template וכך הרשימה שחוזרת כבר מגיעה מהסוג הרצוי:
// get all QPushButton children of a widget
QList<QPushButton *> allPButtons = parentWidget.findChildren<QPushButton *>();

// get all QPushButton that are direct children of widget
QList<QPushButton *> childButtons = parentWidget.findChildren<QPushButton *>(QString(), Qt::FindDirectChildrenOnly);
  1. הפונקציות QObject::parent ו QObject::setParent משמשות לאיתור ושינוי ההורה של אוביקט מסוים.


ל QObject יש מספר פונקציות הקשורות לעץ האוביקטים:

  1. הפונקציה QObject::children מחזירה את רשימת הילדים של אוביקט מסוים. בתוכנית הדוגמא שלנו נוכל להוסיף את שורת ההדפסה הבאה לקוד כדי להיווכח שאכן לאוביקט המסך יש 3 ילדים:
qDebug() << "Widget w has: " << w.children().size() << " children";
  1. הפונקציה QObject::findChildren משמשת לאיתור ילדים לפי שם המחלקה. מעבירים לה את שם המחלקה כמחרוזת ומקבלים רשימה של כל הילדים מסוג מסוים. אפשרות יותר מקובלת היא להשתמש ב Template וכך הרשימה שחוזרת כבר מגיעה מהסוג הרצוי:
// get all QPushButton children of a widget
QList<QPushButton *> allPButtons = parentWidget.findChildren<QPushButton *>();

// get all QPushButton that are direct children of widget
QList<QPushButton *> childButtons = parentWidget.findChildren<QPushButton *>(QString(), Qt::FindDirectChildrenOnly);
  1. הפונקציות QObject::parent ו QObject::setParent משמשות לאיתור ושינוי ההורה של אוביקט מסוים.