QtでGUIアプリーション作成入門(2)

元記事

作成したGUIを使って連絡先を追加できるようにする。

新しい連絡先を追加するためのボタンを加える。さらに、その連絡先をアプリ内で保存するコンテナを加える。

http://qt-project.org/doc/qt-5.1/qtwidgets/images/addressbook-tutorial-part2-add-contact.png

アドレスブック・クラスの定義

Qtではボタンをクリックするなどのユーザーの操作によりシグナルが出される、それを受け取り処理をする関数をスロットと呼ぶ。

アプリのクラス内にボタンなどのGUIクラスと、スロットになる関数を、メンバとして追加する。そして、そのGUIクラスのシグナルと、スロット関数、を結合する。スロットになるメンバ関数は public slot: セクションで宣言する必要がある。

まずは3つのボタンと、それぞれに対応する3つのスロット関数を追加する。

adressbook.h

    //スロット
  public slots:
    void addContact();
    void submitContact();
    void cancel();
    
  private:
    QLineEdit *nameLine;
    QTextEdit *addressText;
    
    //ボタン
    QPushButton *addButton;
    QPushButton *submitButton;
    QPushButton *cancelButton;
    
    //内部データ格納用
    QMap<QString, QString> contacts;
    QString oldName;
    QString oldAddress;
};

3つの QPushButton オブジェクト (addButton, submitButton, cancelButton) をメンバ変数として加える。

アドレスブックの連絡先を格納するコンテナが必要。QMap オブジェクト使う、連絡先は、鍵-値ペアとして保存される:連絡先の名前を鍵, アドレスを値とする。

つの QString オブジェクト (oldName and oldAddress) を宣言する。これらのオブジェクトは最後に表示した入力フォールドの内容を保持する。

アドレスブック・クラスの実装

アドレスブック・クラスのコンストラクタ内で、addButton, submitButton, and cancelButton をインスタンス化して、nameLine と addressText を読み込み専用にセットする。

adressbook.h

class AddressBook : public QWidget
{
    ...
    addButton = new QPushButton(tr("&Add"));
    addButton->show();
    submitButton = new QPushButton(tr("&Submit"));
    submitButton->hide();
    cancelButton = new QPushButton(tr("&Cancel"));
    cancelButton->hide();
    ...
    nameLine->setReadOnly(true);
    addressText->setReadOnly(true);
    ...
}

addButton は show() を呼び出すことで表示されるが、 submitButton と cancelButton は hide() を呼び出すことで隠される。この2つのボタンはユーザーがAddボタンをクリックした時のみ表示されるようにする、これは下で述べるaddContact() 関数内で操作する。

tr()はアプリを複数の言語に対応させるために必要、翻訳するべき文字列を指定するのに使う。ユーザーから見える文字列には常にtr()を付けておくとよい。

次に、各ボタンが押された時の動作を記述する関数(スロット関数)を用意する。スロットして呼び出されるメンバ関数はaddressbookクラス宣言の中で private slots: のスコープ内で宣言する必要がある。
addressbook.h

    private slots:
        void addContact();
        void submitContact();
        void cancel();

addressbook.cpp クラス定義に3つのスロット関数の定義を記述

    void AddressBook::addContact(){}
    void AddressBook::submitContact(){}
    void AddressBook::cancel(){}
AddressBook::AddressBook()

次に、コンストラクタの中で、各プッシュボタン・クラスの clicked() シグナルと、スロットとして呼び出される関数を結びつける。
http://qt-project.org/doc/qt-5.1/qtwidgets/images/addressbook-tutorial-part2-signals-and-slots.png

AddressBook::AddressBook(QWidget *parent)
{
    ...
    //マクロを使った記法
    connect(addButton, SIGNAL(clicked()), this, SLOT(addContact()));
    connect(submitButton, SIGNAL(clicked()), this, SLOT(submitContact()));
    connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancel()));
    //関数へのポインタを使った記法
    connect(addButton, &QPushButton::clicked, this, &AddressBook::addContact);
    connect(submitButton, &QPushButton::clicked, this, &AddressBook::submitContact);
    connect(cancelButton, &QPushButton::clicked, this, &AddressBook::cancel);
    ...
}

次に、プッシュボタンをAddressBookウィジェットの上にレイアウトする。まずはQVBoxLayout を用いてボタンを縦に整列する。

    ...
    QVBoxLayout *buttonLayout1 = new QVBoxLayout;
    buttonLayout1->addWidget(addButton, Qt::AlignTop);
    buttonLayout1->addWidget(submitButton);
    buttonLayout1->addWidget(cancelButton);
    buttonLayout1->addStretch();
    ...

addStretch() は複数のプッシュボタンが、空間内に同じ間隔で配置するのではなく、ボタン同士の間隔は小さくウィジェットの上端に近く配置する。下の図を参照。
http://qt-project.org/doc/qt-5.1/qtwidgets/images/addressbook-tutorial-part2-stretch-effects.png

addLayout().を使って buttonLayout1 をメインレイアウトの(1,2)要素として追加する。これで buttonLayout1をmainLayoutの子レイアウトとして入れ子状に配置することができる。

    QGridLayout *mainLayout = new QGridLayout;
    mainLayout->addWidget(nameLabel, 0, 0);
    mainLayout->addWidget(nameLine, 0, 1);
    mainLayout->addWidget(addressLabel, 1, 0, Qt::AlignTop);
    mainLayout->addWidget(addressText, 1, 1);
    mainLayout->addLayout(buttonLayout1, 1, 2);

ここまででレイアウトは以下のようになる。
http://qt-project.org/doc/qt-5.1/qtwidgets/images/addressbook-tutorial-part2-labeled-layout.png

addContact()

Addボタンを押したら、現在入力フィールドに表示されている連絡先を oldName と oldAddressに保持しておいてから、入力フィールドをクリアした後、読込専用をオフにして入力可能な状態にする。さらに、フォーカスが nameLine にセットされ submitButton と cancelButtonが表示される。

void AddressBook::addContact()
{
    oldName = nameLine->text();
    oldAddress = addressText->toPlainText();

    nameLine->clear();
    addressText->clear();

    nameLine->setReadOnly(false);
    nameLine->setFocus(Qt::OtherFocusReason);
    addressText->setReadOnly(false);

    addButton->setEnabled(false);
    submitButton->show();
    cancelButton->show();
}
submitContact()

submitContact() 関数は3つのパートに分けられる。

1)連絡先の詳細を nameLine と addressText から抽出して、 QString オブジェクトに格納する。フィールドが空なのにSubmitボタンを押さないように確かめる、もし、押したら QMessageBox を表示してnameとaddressを入力するようにメッセージを表示する。
※ QMessageBox::information() の第2引数はメッセージボックスのタイトルだが、Macでは表示されない。

void AddressBook::submitContact()
{
    QString name = nameLine->text();
    QString address = addressText->toPlainText();

    if (name.isEmpty() || address.isEmpty()) {
        QMessageBox::information(this, tr("Empty Field"),
            tr("Please enter a name and address."));
        return;
    }

連絡先が既に登録されていないかチェックする。されていないなら、連絡先を追加し、それを知らせるメッセージを表示する。

    if (!contacts.contains(name)) {
        contacts.insert(name, address);
        QMessageBox::information(this, tr("Add Successful"),
            tr("\"%1\" has been added to your address book.").arg(name));
    } else {
        QMessageBox::information(this, tr("Add Unsuccessful"),
            tr("Sorry, \"%1\" is already in your address book.").arg(name));
        return;
    }

もし、連絡先が既に存在したら、重複して追加しないようにメッセージを表示する。連絡先オブジェクトは鍵-値ペアに基いているので、鍵が一意であることを確保する必要がある。

最後にボタンの表示を通常の状態に復帰させる。

    if (contacts.isEmpty()) {
        nameLine->clear();
        addressText->clear();
    }

    nameLine->setReadOnly(true);
    addressText->setReadOnly(true);
    addButton->setEnabled(true);
    submitButton->hide();
    cancelButton->hide();
}

下の図は我々がユーザーにメッセージを伝えるために使った QMessageBox オブジェクトの表示。
http://qt-project.org/doc/qt-5.1/qtwidgets/images/addressbook-tutorial-part2-add-successful.png

cancel()

cancel() 関数は最後に表示した連絡先詳細を復帰して addButtom を使用可能にし、ssubmitButtonとcancelButtonを隠す。

void AddressBook::cancel()
{
    nameLine->setText(oldName);
    nameLine->setReadOnly(true);

    addressText->setText(oldAddress);
    addressText->setReadOnly(true);

    addButton->setEnabled(true);
    submitButton->hide();
    cancelButton->hide();
}