חיבור אירועים בשיטת Signals and Slots

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

1. מהו Signal

המחלקה QObject כוללת קוד שיודע לחבר באופן דינמי, בזמן ריצה, בין אירוע לקוד הטיפול בו. את פונקצית החיבור connect כבר פגשנו בשעורים קודמים ואזכיר אותה בדוגמא קצרה. הקוד הבא מהקובץ mainwindow.cpp מציג תיבת עריכה ותחתיה תיבת טקסט ובכל פעם שכותבים טקסט בתיבת העריכה הטקסט מועתק גם לתיבת הטקסט. את קוד התוכנית המלא תוכלו למצוא בתיקיית הדוגמאות:

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    QObject::connect(ui->from, &QLineEdit::textChanged,
                     this, &MainWindow::copyTextFromEditToLabel);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::copyTextFromEditToLabel()
{
    ui->to->setText(ui->from->text());
}

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

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

2. החיבור הישן והמאקרו SIGNAL ו SLOT

היכולת להעביר ולשמור מתודות של אוביקטים אחרים בקלות מתבססת על יכולות חדשות של C++11 בהן Qt עושה שימוש החל מגירסא 5. בגרסאות ישנות יותר של Qt כל ניהול הפונקציות בוצע באמצעות מחרוזות טקסט שנשמרו בטבלאות הקריאה. בשביל לחבר פונקציות בכתיב הישן היה עליכם להגדיר את הפונקציה בקובץ h בתור slot באופן הבא:

// in file mainwindow.h

private slots:
    void copyTextFromEditToLabel();

ולאחר מכן להחליף את שורת החיבור לשורה:

// in file mainwindow.cpp, inside constructor

    QObject::connect(ui->from, SIGNAL(textChanged(QString)),
                     this, SLOT(copyTextFromEditToLabel()));

היתרון בכתיב זה שהוא חוסך כתיבה של שם המחלקה בקוד החיבור, ובנוסף הוא מאפשר סיגנלים שיש להם את אותו שם אך פרמטרים שונים (overloading). בכתיב החדש גם אפשר לבצע overload אבל זה כל כך מסורבל שאתם לא רוצים להכנס לשם. ההעדפה היום אצל מפתחי הספריה היא לא להשתמש באותו שם סיגנל עם מספר פרמטרים. בכל מקרה יש כבר הרבה מאוד קוד ישן שכן כתוב בדרך זו ולכן חשוב להכיר את שני התחבירים. חסרון מרכזי של הכתיב הישן הוא שהבדיקה לגבי שם הסיגנל והפרמטרים מתבצעת רק בזמן ריצה, בעוד שבכתיב החדש תוכנית לא תתקמפל אם טעיתם בשם signal, בשם slot או בפרמטרים. זו בלבד סיבה מספיק טובה לבחור בתחביר החדש בכל פעם שמתאפשר.

ב Qt הכינוי SLOT מתיחס לפונקציה שניתן להפעילה כתוצאה מ Signal. בעבר היה צריך לסמן פונקציות מסוימות כ slots, אבל היום עם הכתיב החדש אפשר לחבר כל פונקציה לכל Signal.

3. גישה לפרמטרים של הסיגנל

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

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

void MainWindow::copyTextFromSignal(QString newText)
{
    ui->to->setText(newText);
}

ושורת החיבור נראית כך:

    QObject::connect(ui->from, &QLineEdit::textChanged,
                     this, &MainWindow::copyTextFromSignal);

אבל הדבר המדליק באמת עם סיגנלים ופרמטרים הוא היכולת לחבר ישירות סיגנל לפונקציית טיפול באוביקט Qt ולוותר לחלוטין על הפונקציה המתווכת. יכולת זו מתאפשרת בזכות העיצוב המוצלח של ספריית Qt בה רוב הפונקציות הקשורות לשינוי מצב האוביקט מוגדרות כבר כ slots. בדוגמא של QLabel שראינו, אפשר לחבר ישירות את הפונקציה QLabel::setText לסיגנל QLineEdit::textChanged וכך לוותר על הפונקציה המתווכת:

    QObject::connect(ui->from, &QLineEdit::textChanged,
                     ui->to, &QLabel::setText);

את הרשימה המלאה של סיגנלים לכל אוביקט תוכלו למצוא בתיעוד. לדוגמא עבור QLineEdit הרשימה נראית כך:

Signals
void    cursorPositionChanged(int old, int new)
void    editingFinished()
void    returnPressed()
void    selectionChanged()
void    textChanged(const QString &text)
void    textEdited(const QString &text)

3 signals inherited from QWidget
2 signals inherited from QObject

4. תקציר הפקודות שהוצגו בפרק

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

הפונקציה QObject::connect מחברת קוד טיפול לסיגנל. הכתיב החדש שלה משתמש במצביעים לפונקציה ונראה כך:

    QObject::connect(ui->from, &QLineEdit::textChanged,
                     this, &MainWindow::copyTextFromSignal);

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

    QObject::connect(ui->from, SIGNAL(textChanged(QString)),
                     this, SLOT(copyTextFromEditToLabel()));

מקרה כזה הוא הסיגנל valueChanged של QSpinBox שנשלח גם עם פרמטר int וגם עם פרמטר QString. אפשר לקרוא עוד על הבעיה והפתרון בשאלה בקישור זה.


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

הפונקציה QObject::connect מחברת קוד טיפול לסיגנל. הכתיב החדש שלה משתמש במצביעים לפונקציה ונראה כך:

    QObject::connect(ui->from, &QLineEdit::textChanged,
                     this, &MainWindow::copyTextFromSignal);

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

    QObject::connect(ui->from, SIGNAL(textChanged(QString)),
                     this, SLOT(copyTextFromEditToLabel()));

מקרה כזה הוא הסיגנל valueChanged של QSpinBox שנשלח גם עם פרמטר int וגם עם פרמטר QString. אפשר לקרוא עוד על הבעיה והפתרון בשאלה בקישור זה.