Category Archives: Qt 5 C++

Qt 5 C++ – QtWebEngine

Trong Qt có module QtWebEngine cung cấp các lớp C++ hỗ trợ dựng trang web từ mã HTML, XML, SVG, CSS, Javascript lên một chương trình Qt, dùng cho các hệ điều hành không có sẵn web engine. Đối với các hệ điều hành có sẵn web engine thì chúng ta có thể dùng module QtWebKit, tuy nhiên module này đã bị loại bỏ khỏi Qt 5.5, nên hầu như chúng ta sẽ dùng module QtWebEngine để làm việc với web.

Tổng quan về Qt WebEngine

Kiến trúc:

qtwebengine-architecture

Theo sơ đồ trên thì module QtWebEngine bao gồm những thành phần sau:

  • Qt WebEngine Widgets: cung cấp các thư viện tạo giao diện người dùng với Widget.
  • Qt WebEngine: cung cấp các lớp tạo giao diện người dùng với QtQuick.
  • Qt WebEngine Core: làm việc với Chromium.
  • Qt WebEngine Process: dựng trang, xử lý Javascript.

Module Qt WebEngine Core được xây dựng dựa trên Chromium. Các thành phần của Chromium chịu trách nhiệm trong việc xử lý kết nối mạng, xây dựng trang web, xử lý Javascript, HTML… Ngoài WebEngine thì Qt còn có module WebKit cũng có chức năng thao tác với trang web nhưng rất hạn chế, WebEngine nặng hơn, cung cấp nhiều chức năng thao tác trực tiếp với web hơn WebKit. Dù QtWebEngine được xây dựng trên Chromium nhưng không có nghĩa là module này có các dịch vụ hỗ trợ thêm như của trình duyệt Chrome do Google phát triển.

Cài đặt

Mặc định module QtWebEngine không có sẵn trong Qt Creator sử dụng trình biên dịch MinGW, bạn phải chuyển qua sử dụng Qt Creator dùng trình biên dịch MSVC hoặc sử dụng Add-In trong Visual Studio.

Capture Capture1

Hoặc có một cách khác là biên dịch lại toàn bộ mã nguồn Qt và Chromium sử dụng trình biên dịch MinGW, cách này rất phức tạp nên mình sẽ không đề cập ở đây.

Ngoài ra Visual Studio Add-In cũng không chấp nhận Visual Studio phiên bản Express (hoặc Community) nên cách này cũng khó khả thi (trừ khi bạn dùng crack). Nên tóm lại trong phần này mình sẽ sử dụng Qt Creator với trình biên dịch msvc từ Visual Studio 2015.

Lưu ý: tại thời điểm viết bài này trình biên dịch msvc đi kèm với Qt Creator khi download trên trang chủ không sử dụng được, mình phải cài Visual Studio 2015 Community rồi chỉ định Qt Creator sử dụng trình biên dịch của VS mới chạy được (khi cài Visual Studio 2015 bạn phải tích chọn cài 2 thành phần là Programming Languages→Visual C++Windows and Web Development→Universal Windows App Developments Tools vì đây là 2 thành phần cần thiết để biên dịch trong Qt nhưng không được cài mặc định).

Bây giờ chúng ta sẽ đi tìm hiểu một số chức năng có trong Qt WebEngine.

Mở trang web

Để mở một trang web và hiển thị thì chúng ta dùng lớp QWebEngineView. Chúng ta tạo một project mới.

...
QT += webenginewidgets
...

Trong file .pro chúng ta phải khai báo module QtWebEngine bằng dòng code trên.

#include <QMainWindow>

#include <QWebEngineView>

class Browser : public QMainWindow
{
    Q_OBJECT
public:
    explicit Browser(QWidget *parent = 0);

private:
    QWebEngineView *view;
};

Chúng ta định nghĩa lớp Browser kế thừa từ lớp QMainWindow, bên trong lớp này có một đối tượng QWebEngineView.

#include "browser.h"

Browser::Browser(QWidget *parent) : QMainWindow(parent)
{
    view = new QWebEngineView(this);
    view->load(QUrl("https://phocode.com"));
    setCentralWidget(view);
}

Để tải một trang web về thì chúng ta chỉ cần gọi phương thức load() và truyền vào một đối tượng QUrl. Phương thức setCentralWidget() sẽ thiết lập đối tượng QWebEngineView làm giao diện chính của cửa sổ.

#include "browser.h"
#include <QApplication>
#include <qtwebenginewidgetsglobal.h>

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

    Browser browser;
    browser.show();

    return a.exec();
}

Trong file main.cpp chúng ta chỉ tạo đối tượng Browser rồi gọi phương thức show() như bình thường.

Capture

Một số tính năng của QWebEngineView

Lớp QWebEngineView có sẵn một số tính năng cơ bản của một trình duyệt web. Chúng ta sẽ tìm hiểu qua ví dụ dưới đây:

#include <QMainWindow>
#include <QWebEngineView>
#include <QLineEdit>
#include <QUrl>
#include <QToolBar>

class Browser : public QMainWindow
{
    Q_OBJECT
public:
    explicit Browser(QWidget *parent = 0);

signals:

public slots:
    void adjustLocation();
    void changeLocation();
    void adjustTitle();
    void setProgress(int p);
private:
    QWebEngineView *view;
    QLineEdit   *locationEdit;
    int progress;
};

Chúng ta định nghĩa lại lớp Browser. Lớp QWebEngineView có một số Signal đặc biệt được phát đi trong quá trình duyệt trang web nên chúng ta có thể định nghĩa một số phương thức Slot và thuộc tính để bắt và xử lý các signal đó.

#include "browser.h"

Browser::Browser(QWidget *parent) : QMainWindow(parent)
{
    view = new QWebEngineView(this);
    view->load(QUrl("http://google.com"));
    connect(view, SIGNAL(loadFinished(bool)), this, SLOT(adjustLocation()));
    connect(view, SIGNAL(titleChanged(QString)), this, SLOT(adjustTitle()));
    connect(view, SIGNAL(loadProgress(int)), this, SLOT(setProgress(int)));

    locationEdit = new QLineEdit(this);
    locationEdit->setSizePolicy(QSizePolicy::Expanding, locationEdit->sizePolicy().verticalPolicy());
    connect(locationEdit, SIGNAL(returnPressed()), SLOT(changeLocation()));

    QToolBar *toolBar = addToolBar(tr("Navigation"));
    toolBar->addAction(view->pageAction(QWebEnginePage::Back));
    toolBar->addAction(view->pageAction(QWebEnginePage::Forward));
    toolBar->addAction(view->pageAction(QWebEnginePage::Reload));
    toolBar->addAction(view->pageAction(QWebEnginePage::Stop));
    toolBar->addWidget(locationEdit);

    setCentralWidget(view);
}

void Browser::adjustLocation()
{    
    locationEdit->setText(view->url().toString());
}

void Browser::changeLocation()
{
    QUrl url = QUrl::fromUserInput(locationEdit->text());
    view->load(url);
    view->setFocus();
}

void Browser::adjustTitle()
{
    if(progress <= 0 || progress >= 100)
        setWindowTitle(view->title());
    else
        setWindowTitle(QString("%1 (%2%)").arg(view->title()).arg(progress));
}

void Browser::setProgress(int p)
{
    progress = p;
    adjustTitle();
}

Trong file browser.cpp chúng ta xây dựng lớp Browser thành một trình duyệt web đơn giản.

connect(view, SIGNAL(loadFinished(bool)), this, SLOT(adjustLocation()));
connect(view, SIGNAL(titleChanged(QString)), this, SLOT(adjustTitle()));
connect(view, SIGNAL(loadProgress(int)), this, SLOT(setProgress(int)));

Tín hiệu loadFinished() được phát đi khi QWebEngineView đã tải xong trang web. Chúng ta xử lý bằng phương thức adjustLocation(). 

Tín hiệu titleChanged() được phát đi khi tiêu đề của trang web thay đổi, tức là gần như mỗi lần loadFinished() được phát đi thì titleChanged() cũng được phát đi cùng luôn.

Tín hiệu loadProgress() được phát đi khi mỗi phần tử của trang web được tải xong, ví dụ một trang web mang theo rất nhiều file ảnh, file javascript, file css… mỗi lần những file này được tải xong thì tín hiệu này được phát đi, tín hiệu này mang theo một con số từ 0 đến 100 có nghĩa là trang web đang được tải bao nhiêu %.

locationEdit = new QLineEdit(this);
locationEdit->setSizePolicy(QSizePolicy::Expanding, locationEdit->sizePolicy().verticalPolicy());
connect(locationEdit, SIGNAL(returnPressed()), SLOT(changeLocation()));

Ở đây chúng ta định nghĩa một đối tượng QLineEdit dùng làm thanh địa chỉ của trình duyệt web. Phương thức setSizePolicy() sẽ quy định kích thước của QLineEdit. Tín hiệu returnPressed() được phát đi khi chúng ta gõ phím Enter trên QLineEdit, chúng ta bắt tín hiệu này bằng slot changeLocation().

QToolBar *toolBar = addToolBar(tr("Navigation"));
toolBar->addAction(view->pageAction(QWebEnginePage::Back));
toolBar->addAction(view->pageAction(QWebEnginePage::Forward));
toolBar->addAction(view->pageAction(QWebEnginePage::Reload));
toolBar->addAction(view->pageAction(QWebEnginePage::Stop));
toolBar->addWidget(locationEdit);

Ở đây chúng ta tạo thêm một thanh toolbar, ngoài việc dùng để chứa thanh địa chỉ ở trên, chúng ta còn dùng thêm các đối tượng Action có sẵn của lớp QWebEnginePage, lớp này có thuộc tính QWebEnginePage::WebAction cung cấp một số đối tượng Action có các chức năng cơ bản của một trình duyệt web và thậm chí là có cả icon sẵn. Ví dụ ở trên Back là nút quay lại trang vừa xem, Forward là nút đi tới trang vừa mở, Reload là nút refresh trình duyệt, Stop là nút dừng tải trang.

void Browser::adjustLocation()
{    
    locationEdit->setText(view->url().toString());
}

Phương thức adjustLocation() chỉ đơn giản là thiết lập lại đoạn text trong thanh địa chỉ.

void Browser::changeLocation()
{
    QUrl url = QUrl::fromUserInput(locationEdit->text());
    view->load(url);
    view->setFocus();
}

Phương thức changeLocation() sẽ được gọi khi chúng ta gõ phím Enter trên thanh địa chỉ, lúc này chúng ta tiến hành tải lại trang web.

void Browser::adjustTitle()
{
    if(progress <= 0 || progress >= 100)
        setWindowTitle(view->title());
    else
        setWindowTitle(QString("%1 (%2%)").arg(view->title()).arg(progress));
}

Phương thức adjustTitle() sẽ được gọi khi trang web được tải xong, hoặc một phần của trang web được tải xong, vì ở đây chúng ta chèn thêm số % trang web đã được tải là bao nhiêu.

void Browser::setProgress(int p)
{
    progress = p;
    adjustTitle();
}

Trong lớp Browser chúng ta định nghĩa thuộc tính progress dùng để lưu số % trang web đã được tải. Phương thức setProgress() sẽ được gọi khi QWebEngineView phát tín hiệu loadProgress(), lúc này chúng ta thiết lập thông số mới cho thuộc tính progress, đồng thời gọi lại phương thức adjustTitle().

#include "browser.h"
#include <QApplication>
#include <qtwebenginewidgetsglobal.h>

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

    Browser browser;
    browser.show();

    return a.exec();
}

Trong file main.cpp chúng ta chỉ tạo đối tượng Browser và gọi phương thức show().

Untitled

Qt 5 C++ – Tải file qua mạng với QNetworkAccessManager

Trong phần này chúng ta sẽ học cách gửi các yêu cầu HTTP và nhận file HTML gửi về bằng lớp QNetworkAccessManager.

QNetworkAccessManager

Lớp QNetworkAccessManager cho phép ứng dụng gửi các yêu cầu và nhận trả lời qua mạng. Đối tượng QNetworkAccessManager lưu trữ các phương thức kết nối mạng, cấu hình thường dùng và các thiết lập cần thiết cho các yêu cầu được gửi đi, thông số proxy và cache, các Signal liên quan… Lớp QNetworkAccessManager rất mạnh, gần như bạn chỉ cần tạo một đối tượng QNetworkAccessManager là có thể dùng cho toàn bộ ứng dụng Qt.

Khi bạn tạo một đối tượng QNetworkAccessManager và dùng nó để gửi các yêu cầu qua mạng thì thông điệp gửi về sẽ nằm trong một đối tượng QNetworkReplpy.

Mini Browser

Chúng ta sẽ dùng QNetworkAccessManager để gửi yêu cầu HTTP và lấy về một file HTML.

minibrowser.pro

...
QT += core
QT += gui widgets
QT += webkitwidgets
QT += network
...

Đầu tiên bạn phải chỉ định một số module được dùng trong file .pro bao gồm network, webkitwidgets.

#include <QPushButton>
#include <QGridLayout>
#include <QLineEdit>
#include <QLabel>
#include <QWidget>
#include <QTextBrowser>
#include <QtWebKitWidgets/QWebView>

#include <QVBoxLayout>
#include <QHBoxLayout>

#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>

#include <QObject>

class MiniBrowser : public QWidget
{
    Q_OBJECT
public:
    MiniBrowser(QWidget *parent = 0);

public slots:
    void goToPage();
    void receiveFinishedSignal(QNetworkReply*);
private:
    QPushButton *goBtn;
    QLabel *urlLbl;
    QLineEdit *line;
    QWebView *browser;

    QNetworkAccessManager *manager;
};

Lớp MiniBrowser sẽ là cửa sổ chính.

QWebView *browser;

Lớp QWebView là một widget hỗ trợ hiển thị trang web HTML.

public slots:
    void goToPage();
    void receiveFinishedSignal(QNetworkReply*);

Slot gotoPage() xử lý sự kiện click button. Slot receiveFinishedSignal() xử lý thông điệp trả về của QNetworkAccessManager.

#include "minibrowser.h"
MiniBrowser::MiniBrowser(QWidget *parent) : QWidget(parent)
{
    manager = new QNetworkAccessManager();

    goBtn = new QPushButton("Go", this);
    urlLbl = new QLabel("Url:", this);
    line = new QLineEdit(this);
    browser = new QWebView(this);    

    QVBoxLayout *vbox = new QVBoxLayout(this);
    QHBoxLayout *hbox = new QHBoxLayout(this);

    hbox->addWidget(urlLbl);
    hbox->addWidget(line);
    hbox->addWidget(goBtn);
    vbox->addLayout(hbox);
    vbox->addWidget(browser);

    setLayout(vbox);

    QObject::connect(goBtn, SIGNAL(pressed()), this, SLOT(goToPage()));
    QObject::connect(manager, SIGNAL(finished(QNetworkReply*)), 
                     this, SLOT(receiveFinishedSignal(QNetworkReply*)));
}

void MiniBrowser::goToPage()
{
    QNetworkRequest request(QUrl(this->line->text()));
    manager->get(request);
}

void MiniBrowser::receiveFinishedSignal(QNetworkReply *reply)
{
    if(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) == 302)
    {
        QUrl url = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
        manager->get(QNetworkRequest(url));
    }
    QByteArray array = reply->readAll();
    QString str = QString::fromUtf8(array.data(), array.size());
    this->browser->setHtml(str);
}

Chúng ta sẽ tạo một giao diện của một browser đơn giản và cài đặt các slot xử lý.

manager = new QNetworkAccessManager();

Tạo một đối tượng QNetworkAccessManager.

QNetworkRequest request(QUrl(this->line->text()));
manager->get(request);

Để tiến hành gửi các lệnh request qua mạng thì chúng ta dùng phương thức get(), phương thức này nhận một đối  tượng QNetworkRequest, đây là lớp lưu trữ các thông tin cấu hình chuẩn cho các giao thức ứng dụng, hàm khởi tạo QNetworkRequest nhận một đối tượng QUrl, QUrl chẳng qua chỉ là một lớp lưu trữ thông tin của một đường dẫn Url bình thường.

QObject::connect(manager, SIGNAL(finished(QNetworkReply*)), 
                     this, SLOT(receiveFinishedSignal(QNetworkReply*)));

Sau khi gọi phương thức get(), QNetworkAccessManager sẽ thực hiện việc gửi yêu cầu và chờ thông điệp trả lời và sẽ thông báo qua SIGNAL finished(QNetworkReply*) với dữ liệu đi kèm là một đối tượng QNetworkReply, chúng ta kết nối signal này với slot receiveFinishedSignal() để nhận lấy dữ liệu truyền về trong QNetworkReply.

if(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) == 302)
{
    QUrl url = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
    manager->get(QNetworkRequest(url));
}

Bạn có thể sẽ nhận được một mã lỗi 302 nếu vào đường dẫn google.commã này chuyển hướng bạn đến một đường dẫn khác nên mình lấy đường dẫn mới và gửi một yêu cầu khác.

QByteArray array = reply->readAll();
QString str = QString::fromUtf8(array.data(), array.size());
this->browser->setHtml(str);

Bạn có thể lấy toàn bộ nội dung thông điệp qua phương thức QNetworkReply::readAll(), trong trường hợp này đây là một đoạn code HTML, phương thức này trả về một đối tượng QByteArray() nên nếu bạn muốn đọc kiểu text thì phải chuyển nó qua QString bằng phương thức QString::fromUtf8(). Cuối cùng bạn đưa nội dung HTML vô webview bằng phương thức setHtml().

#include <QApplication>
#include "minibrowser.h"
int main(int argc, char *argv[])
{ 
    QApplication a(argc, argv);
    MiniBrowser browser;
    browser.resize(640, 480);
    browser.setWindowTitle("Mini Browser");
    browser.show();
    return a.exec();
}

Untitled

Nội dung HTML trả về có thể sẽ không đầy đủ hoặc lỗi vì chúng ta không thiết lập một số thông số dành cho giao thức HTTP. Trong bài này mình chỉ hướng dẫn các thao tác cơ bản với lớp QNetworkAccessManager.

 

Qt 5 C++ – Cơ chế hoạt động của Signal và Slot

Trong bài này chúng ta sẽ tìm hiểu về cơ chế hoạt động của Signal và Slot

Trong lập trình GUI thì có một thứ rất quan trọng đó là sự kiện (event), khi một sự kiện nào đó xảy ra thì sẽ có các đối tượng xử lý sự kiện đó. Chẳng hạn như khi click vào nút X trên góc cửa sổ thì thoát chương trình. Qt xử lý sự kiện bằng cách tạo ra Signal và Slot. Trong một số bài trước chúng ta đã tìm hiểu sơ qua về cơ chế này, trong bài này chúng ta sẽ tìm hiểu kỹ hơn.

Signal

Signal tiếng Việt có nghĩa là tín hiệu. Trong Qt, khi một sự kiện nào đó xảy ra, một signal sẽ được phát đi giống như đài truyền hình phát sóng vậy, thực ra nó chỉ là một phương thức của một lớp nhưng không có phần thân hàm {}. Các lớp Widget có sẵn trong Qt có rất nhiều signal được định nghĩa sẵn, và chúng ta cũng có thể viết các signal riêng cho các lớp của chúng ta. Signal không có kiểu trả về, kiểu trả về của signal luôn luôn là void.

Slot

Slot chẳng qua cũng là một phương thức bình thường của một lớp, các phương thức này sẽ được gọi khi có một signal nào đó được phát đi. Cũng giống như signal, các lớp Widget trong Qt cũng có sẵn rất nhiều slot và chúng ta cũng có thể viết slot cho lớp của riêng chúng ta.

Connect

Signal và slot được kết nối qua từng đối tượng (chứ không phải qua từng lớp như nhiều bạn vẫn nghĩ). Tức là chúng ta chỉ có kết nối đối tượng này với đối tượng kia chứ không kết nối lớp này với lớp kia, giả sử chúng ta có đối tượng object1, object2 thuộc lớp A và object3 thuộc lớp B thì chúng ta chỉ có thể kết nối object1->object2, object1->object3 hoặc object3->object2 chứ không kết nối lớp A đến lớp B.

Khi kết nối như vậy thì một đối tượng sẽ làm vai trò phát signal, một đối tượng sẽ nhận signal. Đối tượng phát signal có thể phát các signal và cứ mỗi lần phát như vậy thì đối tượng nhận signal tương ứng sẽ thực thi slot của đối tượng đó.

Một signal có thể kết nối đến nhiều slot và một slot có thể kết nối đến nhiều signal.

Slot sẽ được gọi khi có signal tương ứng được phát ra, nhưng vì slot cũng là một phương thức bình thường như bao phương thức khác nên chúng ta cũng có thể gọi chúng như gọi phương thức bình thường vậy.

Tham số của signal phải ít hơn hoặc bằng tham số của slot. Khi một signal được phát đi, nó sẽ mang theo dữ liệu là các tham số của nó, và slot nhận signal này sẽ nhận các tham số đó thông qua tham số của nó. Thứ tự các tham số của signal và slot phải giống nhau, chẳng hạn như signal gửi 1 int, sau đó là 1 string thì slot cũng phải nhận 1 int rồi mới tới string.

Một signal cũng có thể kết nối đến một signal khác, tức là như thế sẽ phát ra 2 signal.

(ảnh : Qt)

Ví dụ

Chúng ta sẽ viết 2 lớp, một lớp phát signal và một lớp có slot nhận signal.

Emitter

#include <QObject>
#include <QString>

class Emitter : public QObject
{
    Q_OBJECT
public:
    Emitter();

    void emitSignal1();
    void emitSignal2(int);
    void emitSignal3(QString, int);

    void setName(QString);
signals:
    void signal1();
    void signal2(int);
    void signal3(QString, int);
};

Lớp Emitter là lớp phát signal, trong đó có 3 signal khác nhau.

class Emitter : public QObject
{
    Q_OBJECT
    ...
}

Để một lớp có thể sử dụng signal và slot thì lớp đó phải kế thừa từ lớp QObject và ở đầu lớp bạn phải khai báo macro Q_OBJECT.

signals:
    void signal1();
    void signal2(int);
    void signal3(QString, int);

Chúng ta dùng từ khóa signals để báo cho Qt biết phương thức nào là một signal. Các signal sẽ có kiểu trả về là void, và cũng có các tham số đi kèm như một phương thức bình thường.

#include "emitter.h"

Emitter::Emitter() {}

void Emitter::emitSignal1()
{
    emit signal1();
}

void Emitter::emitSignal2(int arg)
{
    emit signal2(arg);
}

void Emitter::emitSignal3(QString arg1, int arg2)
{
    emit signal3(arg1, arg2);
}

void Emitter::setName(QString arg)
{
    this->setObjectName(arg);
}

Các signal sẽ được gọi trong các phương thức khác nhau.

void Emitter::emitSignal1()
{
    emit signal1();
}

Để phát các signal thì bạn dùng từ khóa emit.

void Emitter::setName(QString arg)
{
    this->setObjectName(arg);
}

Lớp QObject có sẵn một thuộc tính là objectName dùng để đặt tên cho đối tượng, thuộc tính này có giá trị rỗng, chúng ta có thể đặt giá trị cho thuộc tính này thông qua phương thức setObjectName().

Receiver

#include <QString>
#include <QObject>
#include <QDebug>
#include <iostream>

#include "emitter.h"
class Receiver : public QObject
{
    Q_OBJECT
public:
    Receiver();

public slots:
    void receiveSignal1();
    void receiveSignal2(int);
    void receiveSignal3(QString, int);
};

Chúng ta viết lớp Receiver có 3 slot xử lý từng signal khác nhau.

public slots:
    void receiveSignal1();
    void receiveSignal2(int);
    void receiveSignal3(QString, int);

Các slot cũng chỉ là các phương thức bình thường. Khi khai báo bạn phải thêm từ khóa slots ở phía trước.

#include "receiver.h"

Receiver::Receiver()
{

}

void Receiver::receiveSignal1()
{
    std::cout << "Signal 1 received!\n";
}

void Receiver::receiveSignal2(int arg)
{
    std::cout << "Signal 2 came with an integer: "
              << arg
              << "\n";
}

void Receiver::receiveSignal3(QString arg1, int arg2)
{ 
     std::cout << "Signal 3 from "
               << QObject::sender()->objectName().toStdString()
               << ": "
               << arg1.toStdString()
               << " "
               << QString::number(arg2).toStdString()
               << "\n";
}

Tham số đi kèm với signal cũng sẽ là tham số được truyền vào lời gọi các phương thức slot tương ứng.

QObject::sender()->objectName().toStdString()

Ngoài ra bạn có thể lấy con trỏ tham chiếu đến đối tượng đã gửi signal thông qua phương thức QObject::sender(). Phương thức objectName() lấy thuộc tính objectName.

arg1.toStdString()

std::cout là đối tượng xuất trong thư viện C++ chuẩn, không liên quan gì đến Qt cả nên chúng ta phải chuyển đối kiểu dữ liệu từ QString của Qt sang std::string của C++ bằng phương thức toStdString().

Main

#include <QCoreApplication>
#include <QObject>

#include "emitter.h"
#include "receiver.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    Emitter e1, e2;
    Receiver r1, r2;

    e1.setName("Emitter 1");
    e2.setName("Emitter 2");

    QObject::connect(&e1, SIGNAL(signal1()), &r1, SLOT(receiveSignal1()));
    e1.emitSignal1();

    QObject::connect(&e2, Emitter::signal2, &r2, Receiver::receiveSignal2);
    e2.emitSignal2(3);

    QObject::connect(&e1, Emitter::signal3, &r1, Receiver::receiveSignal3);
    QObject::connect(&e2, Emitter::signal3, &r1, Receiver::receiveSignal3);
    e1.emitSignal3("Current year is", 2016);
    e2.emitSignal3("Sex is", 0);

    return a.exec();
}

Trong hàm main() chúng ta khai báo các đối tượng Emitter và Receiver để kết nối.

QObject::connect(&e1, SIGNAL(signal1()), &r1, SLOT(receiveSignal1()));
...

QObject::connect(&e2, Emitter::signal2, &r2, Receiver::receiveSignal2);
...

Để kết nối một signal của một đối tượng với một slot của một đối tượng khác thì chúng ta dùng phương thức QObject::connect(), phương thức này có rất nhiều override khác nhau vì Qt đã được phát triển từ rất lâu rồi. Ở trên là 2 cú pháp dùng trong phương thức này.

Signal 1 received!
Signal 2 came with an integer: 3
Signal 3 from Emitter 1 : Current year is 2016
Signal 3 from Emitter 2 : Sex is 0

Qt 5 C++ – Trò chơi phá gạch

Trong phần này chúng ta viết lại một game khác là game phá gạch (Breakout).

Breakout

Game phá gạch được phát riển bởi Atari vào năm 1976. Trong game, người chơi sẽ di chuyển một thanh đỡ cố gắng đỡ quả bóng bay lên phá vỡ đống gạch phía trên. Khi số gạch đã bị phá hết thì người chơi chiến thắng, nếu người chơi không đỡ được quả bóng làm quả bóng rơi xuống dưới thanh đỡ thì người chơi thua.

Mô tả game

Game của chúng ta có một thanh đỡ (Paddle), một quả bóng (Ball) và 30 viên gạch (Brick). Chúng ta sẽ dùng timer để chạy vòng lặp game. Game này chúng ta làm đơn giản thôi nên không cần thêm hệ thống tính điểm vào làm gì.

#pragma once

#include <QImage>
#include <QRect>

class Paddle {

  public:
    Paddle();
    ~Paddle();

  public:
    void resetState();
    void move();
    void setDx(int);
    QRect getRect();
    QImage & getImage();

  private:
    QImage image;
    QRect rect;
    int dx;
    static const int INITIAL_X = 200;
    static const int INITIAL_Y = 360;
};

Đây là file header của lớp paddle. Hai hằng số INITIAL_XINITIAL_Y là tọa độ khởi đầu của paddle.

#include <iostream>
#include "paddle.h"

Paddle::Paddle() {
    
  dx = 0;    
  image.load("paddle.png");
  image = image.scaled(70, 10);
  rect = image.rect();
  resetState();
}

Paddle::~Paddle() {
    
 std::cout << ("Paddle deleted") << std::endl;
}

void Paddle::setDx(int x) {
  dx = x;
}

void Paddle::move() {
    
    int x = rect.x() + dx;
    int y = rect.top();
    
    rect.moveTo(x, y);
}

void Paddle::resetState() {
    
  rect.moveTo(INITIAL_X, INITIAL_Y);
}

QRect Paddle::getRect() {
    
  return rect;
}

QImage & Paddle::getImage() {
    
  return image;
}

Hướng di chuyển của Paddle là trái-phải.

Paddle::Paddle() {
    
  dx = 0;    
  image.load("paddle.png");
  image = image.scaled(70, 10);
  rect = image.rect(); resetState(); 
}

Load ảnh của paddle, bạn có thể lên google search từ khóa “paddle sprite” để tìm ảnh làm mẫu. Sau khi load ảnh lên thì phải resize về kích thước 70×10 vì ảnh gốc của chúng ta có thể lớn hơn kích thước đó. Các bạn có thể resize về kích thước khác nếu thích.

void Paddle::move() {
    
    int x = rect.x() + dx;
    int y = rect.top();
    
    rect.moveTo(x, y);
}

Phương thức  move() di chuyển paddle. Tốc độ di chuyển của paddle được định nghĩa bởi biến dx.

void Paddle::resetState() {
    
  rect.moveTo(INITIAL_X, INITIAL_Y);
}

Phương thức resetState() sẽ đưa paddle về vị trí khởi đầu.

#pragma once

#include <QImage>
#include <QRect>

class Brick {

  public:
    Brick(int, int);
    ~Brick();

  public:
    bool isDestroyed();
    void setDestroyed(bool);
    QRect getRect();
    void setRect(QRect);
    QImage & getImage();

  private:
    QImage image;
    QRect rect;
    bool destroyed;
};

Đây là file header của lớp Brick. Biến destroyed lưu trữ trạng thái hiện tại của brick là còn hay đã bị phá.

#include <iostream>
#include "brick.h"

Brick::Brick(int x, int y) {
    
  image.load("brick.png");
  image = image.scaled(40, 12);
  destroyed = false;
  rect = image.rect();
  rect.translate(x, y);
}

Brick::~Brick() {

  std::cout << ("Brick deleted") << std::endl;
}

QRect Brick::getRect() {
    
  return rect;
}

void Brick::setRect(QRect rct) {
    
  rect = rct;
}

QImage & Brick::getImage() {
    
  return image;
}

bool Brick::isDestroyed() {
    
  return destroyed;
}

void Brick::setDestroyed(bool destr) {
    
  destroyed = destr;
}
Brick::Brick(int x, int y) {
    
  image.load("brickie.png");
  image = image.scaled(40, 12);
  destroyed = false;
  rect = image.rect();
  rect.translate(x, y);
}

Trong phương thức khởi tạo, chúng ta load ảnh của brick, resize về kích thước 40×12 và thiết lập biến destroyed là false.

bool Brick::isDestroyed() {
    
  return destroyed;
}

Phương thức isDestroyed() trả về trạng thái của brick.

#pragma once

#include <QImage>
#include <QRect>

class Ball {

  public:
    Ball();
    ~Ball();

  public:
    void resetState();
    void autoMove();
    void setXDir(int);
    void setYDir(int);
    int getXDir();
    int getYDir();
    QRect getRect();
    QImage & getImage();
  
  private:
    int xdir;
    int ydir;
    QImage image;
    QRect rect;
    static const int INITIAL_X = 230;
    static const int INITIAL_Y = 355;    
    static const int RIGHT_EDGE = 300;
};

Đây là file header của lớp Ball. Hai biến xdirydir lưu trữ hướng đi của quả bóng, hai biến này có giá trị -1 hoặc 1.

#include <iostream>
#include "ball.h"

Ball::Ball() {

  xdir = 1;
  ydir = -1;

  image.load("ball.png");
  image = image.scaled(12, 12);
  rect = image.rect();
  resetState();
}

Ball::~Ball() {
    
  std::cout << ("Ball deleted") << std::endl;
}

void Ball::autoMove() {
    
  rect.translate(xdir, ydir);

  if (rect.left() == 0) {
    xdir = 1;
  }

  if (rect.right() == RIGHT_EDGE) {
    xdir = -1;
  }

  if (rect.top() == 0) {
    ydir = 1;
  }
}

void Ball::resetState() {
    
  rect.moveTo(INITIAL_X, INITIAL_Y);
}

void Ball::setXDir(int x) {
    
  xdir = x;
}

void Ball::setYDir(int y) {
    
  ydir = y;
}

int Ball::getXDir() {
    
  return xdir;
}

int Ball::getYDir() {
    
  return ydir;
}

QRect Ball::getRect() {
    
  return rect;
}

QImage & Ball::getImage() {
    
  return image;
}
xdir = 1;
ydir = -1;

Chúng ta khởi tạo cho quả bóng đi theo hướng đông bắc.

void Ball::autoMove() {
    
  rect.translate(xdir, ydir);

  if (rect.left() == 0) {
    xdir = 1;
  }

  if (rect.right() == RIGHT_EDGE) {
    xdir = -1;
  }

  if (rect.top() == 0) {
    ydir = 1;
  }
}

Phương thức autoMove() được gọi theo vòng lặp của game. Nếu quả bóng chạm với tường trái, phải hay phía trên thì hướng của nó sẽ được thiết lập là hướng ngược lại, còn nếu chạm với bờ tường phía dưới thì game over.

#pragma once

#include <QWidget>
#include <QKeyEvent>
#include "ball.h"
#include "brick.h"
#include "paddle.h"

class Breakout : public QWidget {
    
  public:
    Breakout(QWidget *parent = 0);
    ~Breakout();

  protected:
    void paintEvent(QPaintEvent *);
    void timerEvent(QTimerEvent *);
    void keyPressEvent(QKeyEvent *);
    void keyReleaseEvent(QKeyEvent *);
    void drawObjects(QPainter *);
    void finishGame(QPainter *, QString);
    void moveObjects();

    void startGame();
    void pauseGame();
    void stopGame();
    void victory();
    void checkCollision();

  private:
    int x;
    int timerId;
    static const int N_OF_BRICKS = 30;
    static const int DELAY = 10;
    static const int BOTTOM_EDGE = 400;
    Ball *ball;
    Paddle *paddle;
    Brick *bricks[N_OF_BRICKS];
    bool gameOver;
    bool gameWon;
    bool gameStarted;
    bool paused;
};

Đây là file header của lớp Breakout, lớp này điều khiển toàn bộ game.

void keyPressEvent(QKeyEvent *);
void keyReleaseEvent(QKeyEvent *);

Paddle sẽ được điều khiển bằng các phím mũi tên. Hai phương thức keyPressEvent()keyReleaseEvent() lắng nghe sự kiện bấm phím từ người dùng.

int x;
int timerId;

Biến x lưu trữ tọa độ x hiện tại của paddle.

static const int N_OF_BRICKS = 30;

N_OF_BRICKS là số lượng các brick.

static const int DELAY = 10;

Hằng số DELAY lưu trữ tốc độ của game.

static const int BOTTOM_EDGE = 400;

BOTTOM_EDGE là giới hạn phía dưới cửa sổ, nếu quả bóng chạm vào thì game over.

bool gameOver;
bool gameWon;
bool gameStarted;
bool paused;

Bốn biến ở trên lưu trữ trạng thái hiện thời của game.

#include <QPainter>
#include <QApplication>
#include "breakout.h"

Breakout::Breakout(QWidget *parent)
    : QWidget(parent) {
  
  x = 0;
  gameOver = false;
  gameWon = false;
  paused = false;
  gameStarted = false;
  ball = new Ball();
  paddle = new Paddle();

  int k = 0;
  
  for (int i=0; i<5; i++) {
    for (int j=0; j<6; j++) {
      bricks[k] = new Brick(j*40+30, i*10+50);
      k++; 
    }
  }  
}

Breakout::~Breakout() {
    
 delete ball;
 delete paddle;
 
 for (int i=0; i<N_OF_BRICKS; i++) { 
   delete bricks[i]; 
 } 
} 

void Breakout::paintEvent(QPaintEvent *e) { 
  Q_UNUSED(e); 
  QPainter painter(this); 

  if (gameOver) { 
    finishGame(&painter, "Game lost"); 
  } else if(gameWon) { 
    finishGame(&painter, "Victory"); 
  } else { 
    drawObjects(&painter); 
  } 
} 

void Breakout::finishGame(QPainter *painter, QString message) { 
  QFont font("Courier", 15, QFont::DemiBold); 
  QFontMetrics fm(font); 
  int textWidth = fm.width(message); 
  painter->setFont(font);
  int h = height();
  int w = width();

  painter->translate(QPoint(w/2, h/2));
  painter->drawText(-textWidth/2, 0, message);    
}

void Breakout::drawObjects(QPainter *painter) {
    
  painter->drawImage(ball->getRect(), ball->getImage());
  painter->drawImage(paddle->getRect(), paddle->getImage());

  for (int i=0; i<N_OF_BRICKS; i++) { if (!bricks[i]->isDestroyed()) {
      painter->drawImage(bricks[i]->getRect(), bricks[i]->getImage());
    }
  }      
}

void Breakout::timerEvent(QTimerEvent *e) {
    
  Q_UNUSED(e);  
    
  moveObjects();
  checkCollision();
  repaint();
}

void Breakout::moveObjects() {

  ball->autoMove();
  paddle->move();
}

void Breakout::keyReleaseEvent(QKeyEvent *e) {
    
    int dx = 0;
    
    switch (e->key()) {
        case Qt::Key_Left:
            dx = 0;
            paddle->setDx(dx);        
            break;       
            
        case Qt::Key_Right:
            dx = 0;
            paddle->setDx(dx);        
            break;    
    }
}

void Breakout::keyPressEvent(QKeyEvent *e) {
    
    int dx = 0;
    
    switch (e->key()) {
    case Qt::Key_Left:
        
        dx = -1;
        paddle->setDx(dx);
        
        break;
       
    case Qt::Key_Right:
    
        dx = 1;
        paddle->setDx(dx);        
        break;
    
    case Qt::Key_P:
    
        pauseGame();
        break;
        
    case Qt::Key_Space:

        startGame();
        break;        
                
    case Qt::Key_Escape:
        
        qApp->exit();
        break;
        
    default:
        QWidget::keyPressEvent(e);
    }
}

void Breakout::startGame() {
     
  if (!gameStarted) {
    ball->resetState();
    paddle->resetState();

    for (int i=0; i<N_OF_BRICKS; i++) { bricks[i]->setDestroyed(false);
    }
    
    gameOver = false; 
    gameWon = false; 
    gameStarted = true;
    timerId = startTimer(DELAY);  
  }      
}

void Breakout::pauseGame() {
    
  if (paused) {
      
    timerId = startTimer(DELAY);
    paused = false;
  } else {
      
    paused = true;
    killTimer(timerId); 
  }        
}

void Breakout::stopGame() {
    
  killTimer(timerId);    
  gameOver = true;      
  gameStarted = false;
}

void Breakout::victory() {
    
  killTimer(timerId);    
  gameWon = true;  
  gameStarted = false;    
}

void Breakout::checkCollision() {
  
  if (ball->getRect().bottom() > BOTTOM_EDGE) {
    stopGame();
  }

  for (int i=0, j=0; i<N_OF_BRICKS; i++) { if (bricks[i]->isDestroyed()) {
      j++;
    }
    
    if (j == N_OF_BRICKS) {
      victory();
    }
  }

  if ((ball->getRect()).intersects(paddle->getRect())) {

    int paddleLPos = paddle->getRect().left();  
    int ballLPos = ball->getRect().left();   

    int first = paddleLPos + 8;
    int second = paddleLPos + 16;
    int third = paddleLPos + 24;
    int fourth = paddleLPos + 32;

    if (ballLPos < first) { ball->setXDir(-1);
      ball->setYDir(-1);
    }

    if (ballLPos >= first && ballLPos < second) { ball->setXDir(-1);
      ball->setYDir(-1*ball->getYDir());
    }

    if (ballLPos >= second && ballLPos < third) { ball->setXDir(0);
       ball->setYDir(-1);
    }

    if (ballLPos >= third && ballLPos < fourth) { ball->setXDir(1);
       ball->setYDir(-1*ball->getYDir());
    }

    if (ballLPos > fourth) {
      ball->setXDir(1);
      ball->setYDir(-1);
    }
  }      
 
  for (int i=0; i<N_OF_BRICKS; i++) { 
      if ((ball->getRect()).intersects(bricks[i]->getRect())) {

      int ballLeft = ball->getRect().left();  
      int ballHeight = ball->getRect().height(); 
      int ballWidth = ball->getRect().width();
      int ballTop = ball->getRect().top();  
  
      QPoint pointRight(ballLeft + ballWidth + 1, ballTop);
      QPoint pointLeft(ballLeft - 1, ballTop);  
      QPoint pointTop(ballLeft, ballTop -1);
      QPoint pointBottom(ballLeft, ballTop + ballHeight + 1);  

      if (!bricks[i]->isDestroyed()) {
        if(bricks[i]->getRect().contains(pointRight)) {
           ball->setXDir(-1);
        } 

        else if(bricks[i]->getRect().contains(pointLeft)) {
           ball->setXDir(1);
        } 

        if(bricks[i]->getRect().contains(pointTop)) {
           ball->setYDir(1);
        } 

        else if(bricks[i]->getRect().contains(pointBottom)) {
           ball->setYDir(-1);
        } 

        bricks[i]->setDestroyed(true);
      }
    }
  }
}
int k = 0;
for (int i=0; i<5; i++) {
  for (int j=0; j<6; j++) {
    bricks[k] = new Brick(j*40+30, i*10+50);
    k++; 
  }
}

Trong phương thức khởi tạo của lớp Breaout, chúng ta khởi tạo 30 viên gạch trên 5 hàng 6 cột.

void Breakout::paintEvent(QPaintEvent *e) {
  
  Q_UNUSED(e);  
    
  QPainter painter(this);

  if (gameOver) {
  
    finishGame(&painter, "Game lost");    

  } else if(gameWon) {

    finishGame(&painter, "Victory");
  }
  else {
      
    drawObjects(&painter);
  }
}

Trong phương thức paintEvent(), nếu trạng thái của game là đang chơi thì chúng ta vẽ paddle, ball và brick, nếu không phải thì chúng ta vẽ các đoạn chữ “Game lost”, “Victory” tùy trường hợp.

void Breakout::finishGame(QPainter *painter, QString message) {
    
  QFont font("Courier", 15, QFont::DemiBold);
  QFontMetrics fm(font);
  int textWidth = fm.width(message);

  painter->setFont(font);
  int h = height();
  int w = width();

  painter->translate(QPoint(w/2, h/2));
  painter->drawText(-textWidth/2, 0, message);    
}

Trong phương thức finishGame() cũng vậy, phương thức này vẽ đoạn chữ trên màn hình và tùy vào game đã thắng hay thua mà chúng ta vẽ cho phù hợp.

void Breakout::drawObjects(QPainter *painter) {
    
  painter->drawImage(ball->getRect(), ball->getImage());
  painter->drawImage(paddle->getRect(), paddle->getImage());

  for (int i=0; i<N_OF_BRICKS; i++) { if (!bricks[i]->isDestroyed()) {
      painter->drawImage(bricks[i]->getRect(), bricks[i]->getImage());
    }
  }      
}

Phương thức drawObjects() vẽ tất cả các đối tượng có trong game.

void Breakout::timerEvent(QTimerEvent *e) {
    
  Q_UNUSED(e);  
    
  moveObjects();
  checkCollision();
  repaint();
}

Phương thức timerEvent(), là vòng lặp chính của game, cứ mỗi lần lặp, chúng ta vẽ các đối tượng, kiểm tra sự va chạm của các đối tượng và gọi phương thức repaint().

void Breakout::moveObjects() {

  ball->autoMove();
  paddle->move();
}

Phương thức moveObjects() di chuyển quả bóng và thanh đỡ.

void Breakout::keyReleaseEvent(QKeyEvent *e) {
    
    int dx = 0;
    
    switch (e->key()) {
        case Qt::Key_Left:
            dx = 0;
            paddle->setDx(dx);        
            break;       
            
        case Qt::Key_Right:
            dx = 0;
            paddle->setDx(dx);        
            break;    
    }
}

Khi người chơi thả phím thì thanh đỡ ngừng di chuyển, chúng ta thiết lập với dx về 0.

void Breakout::keyPressEvent(QKeyEvent *e) {
    
    int dx = 0;
    
    switch (e->key()) {
    case Qt::Key_Left:
        
        dx = -1;
        paddle->setDx(dx);
        
        break;
       
    case Qt::Key_Right:
    
        dx = 1;
        paddle->setDx(dx);        
        break;
    
    case Qt::Key_P:
    
        pauseGame();
        break;
        
    case Qt::Key_Space:

        startGame();
        break;        
                
    case Qt::Key_Escape:
        
        qApp->exit();
        break;
        
    default:
        QWidget::keyPressEvent(e);
    }
}

Tại phương thức keyPressEvent(), chúng ta lắng nghe các sự kiện bấm phím. Cữ mỗi lần bấm các phím mũi tên trái hoặc phải, chúng ta set biến dx về -1 hoặc 1. Nếu bấm phím P thì game sẽ tạm dừng, nếu bấm phím Esc thì thoát toàn bộ game, bấm phím Space thì game sẽ bắt đầu chơi.

void Breakout::startGame() {
     
  if (!gameStarted) {
    ball->resetState();
    paddle->resetState();

    for (int i=0; i<N_OF_BRICKS; i++) { 
      bricks[i]->setDestroyed(false);
    }
    
    gameOver = false; 
    gameWon = false; 
    gameStarted = true;
    timerId = startTimer(DELAY);  
  }      
}

Phương thức startGame() reset lại vị trí paddle, vị trí ball, thiết lập trạng thái của các brick về lúc chưa bị hủy và chạy timer.

void Breakout::pauseGame() {
    
  if (paused) {
      
    timerId = startTimer(DELAY);
    paused = false;
  } else {
      
    paused = true;
    killTimer(timerId); 
  }        
}

Phương thức pauseGame() được dùng để dừng game hoặc cho game tiếp tục chạy. Trạng thái dừng của game được lưu trong biến paused. Để dừng game thì chúng ta chỉ đơn giản là dừng vòng lặp chứ không thay đổi gì trong các phương thức di chuyển của các lớp kia.

void Breakout::stopGame() {
    
  killTimer(timerId);    
  gameOver = true;      
  gameStarted = false;
}

Phương thức stopGame() sẽ hủy timer và thiết lập các biến trạng thái cho phù hợp với trạng thái của game.

void Breakout::checkCollision() {
  
  if (ball->getRect().bottom() > BOTTOM_EDGE) {
    stopGame();
  }
...
}

Trong phương thức checkCollision(), chúng ta kiểm tra sự va chạm giữa các đối tượng. Nếu quả bóng chạm tới đáy màn hình thì game kết thúc.

for (int i=0, j=0; i<N_OF_BRICKS; i++) { if (bricks[i]->isDestroyed()) {
    j++;
  }
    
  if (j == N_OF_BRICKS) {
    victory();
  }
}

Chúng ta kiểm tra xem có bao nhiêu viên gạch đã bị hủy, nếu số gạch bị hủy bằng 30 thì người chơi chiến thắng.

#include <QApplication>
#include "breakout.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  Breakout window;
  
  window.resize(300, 400);
  window.setWindowTitle("Breakout");
  window.show();

  return app.exec();
}
Untitled

Qt 5 C++ – Trò chơi rắn săn mồi

Trong phần này chúng ta sẽ viết lại một game cổ điển đó là game rắn săn mồi (Snake Game).

Snake

Game này được phát hành vào những năm 70 trên các hệ máy cầm tay, một thời gian sau được đưa lên PC. Trong game người chơi sẽ điều khiển một con rắn. Mục tiêu của người chơi là cố gắng cho con rắn ăn được càng nhiều mồi càng tốt. Mỗi lần con rắn ăn mồi, cơ thể nó sẽ dài ra. Game kết thúc khi người chơi cho rắn “ăn” phải tường hoặc cơ thể chính mình.

Mô tả game

Kích thước của mỗi đốt trên con rắn là 10px. Con rắn sẽ được điều khiển bằng các phím mũi tên. Khi game kết thúc, dòng chữ “Game Over” sẽ hiện lên giữa màn hình.

#pragma once

#include <QWidget>
#include <QKeyEvent>

class Snake : public QWidget {
    
  public:
      Snake(QWidget *parent = 0);

  protected:
      void paintEvent(QPaintEvent *);
      void timerEvent(QTimerEvent *);
      void keyPressEvent(QKeyEvent *);

  private:    
      QImage apple;
    
      static const int B_WIDTH = 300;
      static const int B_HEIGHT = 300;
      static const int DOT_SIZE = 10;
      static const int ALL_DOTS = 900;
      static const int RAND_POS = 29;
      static const int DELAY = 140;    
      
      int timerId;
      int dots;
      int apple_x;
      int apple_y;      
      
      int x[ALL_DOTS]; 
      int y[ALL_DOTS]; 
      
      int startAngle = 0;
      int spanAngle = 16 * 360;
      bool leftDirection;
      bool rightDirection;
      bool upDirection;
      bool downDirection;
      bool inGame;
      
      void loadImages();
      void initGame();
      void locateApple();
      void checkApple();
      void checkCollision();
      void move();
      void doDrawing();
      void gameOver(QPainter &);      
};
static const int B_WIDTH = 300;
static const int B_HEIGHT = 300;
static const int DOT_SIZE = 10;
static const int ALL_DOTS = 900;
static const int RAND_POS = 29;
static const int DELAY = 140;  

Ý nghĩa của các hằng số trên như sau:

  • B_WIDTHB_HEIGHT là kích thước cửa sổ chính.
  • DOT_SIZE là kích thước của mồi và mỗi đốt trên con rắn.
  • ALL_DOTS là số lượng đốt tối đa của rắn. Vì cửa sổ có kích thước 300 * 300, mỗi đốt rắn có kích thước 10 * 10 nến số lượng đốt tối đa là (300 * 300) / (10 * 10) = 900.
  • RAND_POS là hằng số để tính vị trí ngẫu nhiên của mồi.
  • DELAY là tốc độ của game.
int x[ALL_DOTS]; 
int y[ALL_DOTS]; 

Hai mảng x, y lưu trữ vị trí của toàn bộ đốt của con rắn.

#include <QPainter>
#include <QTime>
#include "snake.h"

Snake::Snake(QWidget *parent) : QWidget(parent) {

    setStyleSheet("background-color:black;");
    leftDirection = false;
    rightDirection = true;
    upDirection = false;
    downDirection = false;
    inGame = true;

    resize(B_WIDTH, B_HEIGHT);
    loadImages();
    initGame();
}

void Snake::loadImages() {   
    apple.load("apple.png");
    apple = apple.scaled(DOT_SIZE, DOT_SIZE);
}

void Snake::initGame() {

    dots = 3;

    for (int z = 0; z < dots; z++) {
        x[z] = 50 - z * 10;
        y[z] = 50;
    }

    locateApple();

    timerId = startTimer(DELAY);
}
    
void Snake::paintEvent(QPaintEvent *e) {
  
    Q_UNUSED(e);  

    doDrawing();
}    
    
void Snake::doDrawing() {
    
    QPainter qp(this);
    
    if (inGame) {

        qp.drawImage(apple_x, apple_y, apple);

        for (int z = 0; z < dots; z++) { 
             if (z == 0) { 
                 qp.setBrush(QBrush("red")); 
                 qp.drawChord(x[z], y[z], DOT_SIZE, DOT_SIZE, startAngle, spanAngle); 
             } else { 
                 qp.setBrush(QBrush("green")); 
                 qp.drawChord(x[z], y[z], DOT_SIZE, DOT_SIZE, startAngle, spanAngle); 
             } 
        } 
     } else { 
        gameOver(qp); 
     } 
} 

void Snake::gameOver(QPainter &qp) { 
     QString message = "Game over"; 
     QFont font("Courier", 15, QFont::DemiBold); 
     QFontMetrics fm(font); 

     int textWidth = fm.width(message); 
     qp.setFont(font); 
     int h = height(); 
     int w = width(); 

     qp.setPen(QPen(QBrush("white"), 1)); 
     qp.translate(QPoint(w/2, h/2)); 
     qp.drawText(-textWidth/2, 0, message); 
} 

void Snake::checkApple() { 
     if ((x[0] == apple_x) && (y[0] == apple_y)) { 
          dots++; 
          locateApple(); 
     } 
} 

void Snake::move() { 
    for (int z = dots; z > 0; z--) {
        x[z] = x[(z - 1)];
        y[z] = y[(z - 1)];
    }

    if (leftDirection) {
        x[0] -= DOT_SIZE;
    }

    if (rightDirection) {
        x[0] += DOT_SIZE;
    }

    if (upDirection) {
        y[0] -= DOT_SIZE;
    }

    if (downDirection) {
        y[0] += DOT_SIZE;
    }
}

void Snake::checkCollision() {

    for (int z = dots; z > 0; z--) {

        if ((z > 4) && (x[0] == x[z]) && (y[0] == y[z])) {
            inGame = false;
        }
    }

    if (y[0] >= B_HEIGHT) {
        inGame = false;
    }

    if (y[0] < 0) { 
        inGame = false; 
    } 

    if (x[0] >= B_WIDTH) {
        inGame = false;
    }

    if (x[0] < 0) { 
        inGame = false; 
    } 

    if(!inGame) { 
        killTimer(timerId); 
    } 
} 

void Snake::locateApple() { 
    QTime time = QTime::currentTime(); 
    qsrand((uint) time.msec()); 
    int r = qrand() % RAND_POS; 
    apple_x = (r * DOT_SIZE); 
    r = qrand() % RAND_POS; 
    apple_y = (r * DOT_SIZE); 
} 

void Snake::timerEvent(QTimerEvent *e) { 
    Q_UNUSED(e); 
    if (inGame) { 
        checkApple(); 
        checkCollision(); 
        move(); 
    } 

    repaint(); 
} 

void Snake::keyPressEvent(QKeyEvent *e) { 
    int key = e->key();
    
    if ((key == Qt::Key_Left) && (!rightDirection)) {
        leftDirection = true;
        upDirection = false;
        downDirection = false;
    }

    if ((key == Qt::Key_Right) && (!leftDirection)) {
        rightDirection = true;
        upDirection = false;
        downDirection = false;
    }

    if ((key == Qt::Key_Up) && (!downDirection)) {
        upDirection = true;
        rightDirection = false;
        leftDirection = false;
    }

    if ((key == Qt::Key_Down) && (!upDirection)) {
        downDirection = true;
        rightDirection = false;
        leftDirection = false;
    }    
    
    QWidget::keyPressEvent(e);    
}
void Snake::loadImages() {
    
    apple.load("apple.png");
    apple = apple.scaled(DOT_SIZE, DOT_SIZE);
}

Phương thức loadImages() load ảnh quả táo dùng làm mồi cho con rắn. Vì ảnh chúng ta dùng có thể có kích thước khác 10*10 nên ta phải resize kích thước ảnh lại bằng phương thức scaled().

void Snake::initGame() {

    dots = 3;

    for (int z = 0; z < dots; z++) {
        x[z] = 50 - z * 10;
        y[z] = 50;
    }

    locateApple();

    timerId = startTimer(DELAY);
}

Trong phương thức initGame() chúng ta cho khởi tạo game, bao gồm khởi tạo số lượng đốt của rắn, khởi tạo vị trí ngẫu nhiên của mồi và chạy timer.

void Snake::checkApple() {

    if ((x[0] == apple_x) && (y[0] == apple_y)) {

        dots++;
        locateApple();
    }
}

Bên trong phương thức checkApple() chúng ta kiểm tra xem nếu vị trí đầu rắn có trùng khớp với vị trí của mồi thì chúng ta tăng số lượng đốt lên và khởi tạo mồi mới.

void Snake::move() {

    for (int z = dots; z > 0; z--) {
        x[z] = x[(z - 1)];
        y[z] = y[(z - 1)];
    }

    if (leftDirection) {
        x[0] -= DOT_SIZE;
    }

    if (rightDirection) {
        x[0] += DOT_SIZE;
    }

    if (upDirection) {
        y[0] -= DOT_SIZE;
    }

    if (downDirection) {
        y[0] += DOT_SIZE;
    }
}

Phương thức move() cập nhật vị trí mới của con rắn, các đốt của con rắn sẽ có vị trí mới là vị trí của đốt phía trước con rắn, đối với đốt đầu tiên thì chúng ta dựa vào hướng di chuyển hiện tại để tính vị trí mới.

Phương thức checkCollision() kiểm tra xem nếu con rắn có đụng đầu vào tường hay cắn chính mình hay không.

for (int z = dots; z > 0; z--) {

    if ((z > 4) && (x[0] == x[z]) && (y[0] == y[z])) {
        inGame = false;
    }
}

Nếu con rắn cắn chính mình thì game over.

if (y[0] >= B_HEIGHT) {
    inGame = false;
}

Nếu con rắn chạm tường thì cũng game over.

void Snake::timerEvent(QTimerEvent *e) {
    
    Q_UNUSED(e);  
    
    if (inGame) {

        checkApple();
        checkCollision();
        move();
    }

    repaint();
}  

Phương thức timerEvent() là vòng lặp của game, cứ mỗi lần lặp chúng ta kiểm tra con rắn có ăn mồi, đụng tường hay cắn chính mình hay không và cập nhật vị trí con rắn.

if ((key == Qt::Key_Left) && (!rightDirection)) {
    leftDirection = true;
    upDirection = false;
    downDirection = false;
}

Nếu bấm phím mũi tên trái và hướng đi của con rắn không phải hướng ngược lại – tức bên phải thì chúng ta cập nhật lại hướng đi mới của con rắn.

#include <QApplication>
#include "snake.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);
  
  Snake window;

  window.setWindowTitle("Snake");
  window.show();

  return app.exec();
}
Untitled

Qt 5 C++ – Tùy biến Widget

Trong phần này chúng ta sẽ học cách tùy biến một widget cho riêng mình.

Những IDE giúp phát triển phần mềm như Visual Studio hầu hết đều chỉ cung cấp một tập các widget có sẵn như button, label, slider… Đôi khi chúng ta cần những widget làm các công việc đặc thù riêng. Lúc đó chúng ta có hai cách, một là code lại widget từ đầu, hai là phát triển từ các widget có sẵn trong IDE. Nếu widget mà chúng ta cần không có gì lớn lắm thì chúng ta nên dùng các widget có sẵn để tiết kiệm thời gian.

Burning widget

Để ví dụ cho bài này, chúng ta sẽ tạo widget hiển thị “thanh burn”, cái này các bạn nào thường dùng các phần mềm burn đĩa CD/DVD như Nero sẽ thấy (có lẽ tại thời điểm viết bài này thì chẳng còn mấy ai dùng đĩa nữa).

#pragma once

#include <QWidget>
#include <QSlider>
#include <QFrame>
#include "widget.h"

class Burning : public QFrame {

  Q_OBJECT  

  public:
    Burning(QWidget *parent = 0);
    int getCurrentWidth();
    
  public slots:
    void valueChanged(int);
    
  private:
    QSlider *slider;
    Widget *widget;
    int cur_width;
    
    void initUI();
};
</pre>
<pre class="explanation">[code language="java" title=""]
public:
  Burning(QWidget *parent = 0);
  int getCurrentWidth();

Phương thức getCurrentWidth() dùng để lấy giá trị hiện tại của slider.

private:
  QSlider *slider;
  Widget *widget;
  int cur_width;
    
  void initUI();

Chúng ta sẽ hiển thị 2 widget lên cửa sổ chính, một slider và một widget do chúng ta tự thiết kế. Giá trị max của slider là 750, biến cur_width là biến lưu trữ giá trị hiện tại của slider, mỗi lần biến này thay đổi, chúng ta sẽ thay đổi lớp Widget (do chúng ta tự thiết kế).

#include <QVBoxLayout>
#include <QHBoxLayout>
#include "burning.h"

Burning::Burning(QWidget *parent)
    : QFrame(parent) {
           
  initUI();
}

void Burning::initUI() {
    
  const int MAX_VALUE = 750; 
  cur_width = 0; 
  
  slider = new QSlider(Qt::Horizontal , this); 
  slider->setMaximum(MAX_VALUE);
  slider->setGeometry(50, 50, 130, 30);

  connect(slider, &QSlider::valueChanged, this, &Burning::valueChanged);
  
  QVBoxLayout *vbox = new QVBoxLayout(this);
  QHBoxLayout *hbox = new QHBoxLayout();

  vbox->addStretch(1);

  widget = new Widget(this);  
  hbox->addWidget(widget, 0);

  vbox->addLayout(hbox);

  setLayout(vbox);    
}

void Burning::valueChanged(int val) {
    
  cur_width = val;
  widget->repaint();
}

int Burning::getCurrentWidth() {
    
  return cur_width;
}

Lớp Burning là lớp hiển thị cửa sổ chính.

connect(slider, &QSlider::valueChanged, this, &Burning::valueChanged);

Chúng ta connect signal valueChanged() của slider với slot valueChanged() của lớp Burning, ở đây signal là có sẵn trong Qt, còn slot là do chúng ta viết thêm, mỗi lần thanh trượt trên slider thay đổi, slot valueChanged() của Burning sẽ được gọi.

void Burning::valueChanged(int val) {

  cur_width = val;
  widget->repaint();
}

Mỗi lần gọi phương thức valueChanged(), chúng ta cập nhật giá trị của slider và gọi phương thức repaint() của lớp Widget.

#pragma once

#include <QFrame>

class Burning;

class Widget : public QFrame {

  public:
    Widget(QWidget *parent = 0);

  protected:
    void paintEvent(QPaintEvent *e);
    void drawWidget(QPainter &qp);

  private:
    QWidget *m_parent;
    Burning *burn;
    
    static const int DISTANCE = 19;
    static const int LINE_WIDTH = 5;
    static const int DIVISIONS = 10;
    static const float FULL_CAPACITY = 700;
    static const float MAX_CAPACITY = 750;     
};
private:
  QWidget *m_parent;
  Burning *burn;

Ở lớp Widget, chúng ta dùng một con trỏ QWidget để tham chiếu đến widget cha của nó, widget cha ở đây chính là lớp Burning, nhờ vậy chúng ta mới có thể truy xuất được biến cur_width của widget cha từ widget con.

const int DISTANCE = 19;
const int LINE_WIDTH = 5;
const int DIVISIONS = 10;
const float FULL_CAPACITY = 700;
const float MAX_CAPACITY = 750;

Ý nghĩa của các hằng số này như sau:

  • DISTANCE là tọa độ trục tung-y của lớp Widget trên cửa sổ.
  • LINE_WIDTH là chiều dài của các đường kẻ dọc bên trên các chữ số.
  • DIVISIONS là số lượng các phần được chia ra bởi các chữ số.
  • FULL_CAPACITY là khoảng giá trị của phần màu vàng, khi giá trị của slider vượt quá FULL_CAPACITY thì Widget sẽ chuyển sang tô màu đỏ.
  • MAX_CAPACITY là giá trị tối đa của slider.
#include <QtGui>
#include "widget.h"
#include "burning.h"

const int PANEL_HEIGHT = 30;

Widget::Widget(QWidget *parent)
    : QFrame(parent) {
        
  m_parent = parent;
  setMinimumHeight(PANEL_HEIGHT);
}

void Widget::paintEvent(QPaintEvent *e) {
  
  QPainter qp(this);
  drawWidget(qp);
  
  QFrame::paintEvent(e);  
}

void Widget::drawWidget(QPainter &qp) {
    
  QString num[] = { "75", "150", "225", "300", "375", "450", 
    "525", "600", "675" };    
  
  int asize = sizeof(num)/sizeof(num[1]); 
  
  QColor redColor(255, 175, 175);
  QColor yellowColor(255, 255, 184);  
  
  int width = size().width();

  Burning *burn = (Burning *) m_parent;
  int cur_width = burn->getCurrentWidth();

  int step = (int) qRound((double)width / DIVISIONS);
  int till = (int) ((width / MAX_CAPACITY) * cur_width);
  int full = (int) ((width / MAX_CAPACITY) * FULL_CAPACITY);

  if (cur_width >= FULL_CAPACITY) {
      
    qp.setPen(yellowColor); 
    qp.setBrush(yellowColor);
    qp.drawRect(0, 0, full, 30);
    qp.setPen(redColor);
    qp.setBrush(redColor);
    qp.drawRect(full, 0, till-full, PANEL_HEIGHT);

  } else if (till > 0) {
       
    qp.setPen(yellowColor);
    qp.setBrush(yellowColor);
    qp.drawRect(0, 0, till, PANEL_HEIGHT);
  }

  QColor grayColor(90, 80, 60);
  qp.setPen(grayColor);
  
  for (int i=1; i <=asize; i++) {
      
    qp.drawLine(i*step, 0, i*step, LINE_WIDTH);
    QFont newFont = font();
    newFont.setPointSize(7);
    setFont(newFont);

    QFontMetrics metrics(font());

    int w = metrics.width(num[i-1]);
    qp.drawText(i*step-w/2, DISTANCE, num[i-1]);
  }
}

Chúng ta vẽ Widget bằng cách vẽ hình chữ nhật có tô màu, các đường kẻ dọc và các chữ số.

int width = size().width(); 

Phương thức size().width() sẽ lấy độ lớn của cửa sổ. Bởi vì người dùng có thể resize kích thước cửa sổ, do đó chúng ta lấy kích thước cửa sổ để resize Widget của chúng ta theo cửa sổ chính.

Burning *burn = (Burning *) m_parent;
int cur_width = burn->getCurrentWidth();

Lấy giá trị hiện tại của slider.

int till = (int) ((width / MAX_CAPACITY) * cur_width);
int full = (int) ((width / MAX_CAPACITY) * FULL_CAPACITY); 

Khi kích thước cửa sổ thay đổi, tọa độ để vẽ phần màu vàng và đỏ cũng phải thay đổi vì chúng ta vẽ theo tọa độ pixel.

qp.setPen(redColor);
qp.setBrush(redColor);
qp.drawRect(full, 0, till-full, PANEL_HEIGHT); 

Vẽ phần hình chữ nhật màu đỏ.

qp.drawLine(i*step, 0, i*step, LINE_WIDTH); 

Vẽ các đường kẻ dọc.

QFontMetrics metrics(font());

int w = metrics.width(num[i-1]);
qp.drawText(i*step-w/2, DISTANCE, num[i-1]);

Vẽ các chữ số, cũng giống như phần màu vàng-đỏ, tọa độ của các chữ số cũng được tính toán lại để phù hợp với thiết kế.

#include <QApplication>
#include "burning.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);
  Burning window;

  window.resize(370, 200);
  window.setWindowTitle("The Burning widget");
  window.show();

  return app.exec();
}
Capture

Qt 5 C++ – Painting

Trong phần này chúng ta sẽ học cách vẽ trong Qt. Lớp QPainter với phương thức paintEvent() là lớp chính để vẽ tất cả mọi thứ.

Đường thẳng

Để bắt đầu chúng ta sẽ vẽ vài đường thẳng đơn giản.

#pragma once

#include <QWidget>

class Lines : public QWidget {

  public:
    Lines(QWidget *parent = 0);

  protected:
    void paintEvent(QPaintEvent *event);
    void drawLines(QPainter *qp);
};
#include <QPainter>
#include "lines.h"

Lines::Lines(QWidget *parent)
    : QWidget(parent)
{ }

void Lines::paintEvent(QPaintEvent *e) {
  QPainter qp(this);
  drawLines(&qp);
}

void Lines::drawLines(QPainter *qp) {
  
  QPen pen(Qt::black, 2, Qt::SolidLine);  
  qp->setPen(pen);
  qp->drawLine(20, 40, 250, 40);

  pen.setStyle(Qt::DashLine);
  qp->setPen(pen);
  qp->drawLine(20, 80, 250, 80);

  pen.setStyle(Qt::DashDotLine);
  qp->setPen(pen);
  qp->drawLine(20, 120, 250, 120);

  pen.setStyle(Qt::DotLine);
  qp->setPen(pen);
  qp->drawLine(20, 160, 250, 160);

  pen.setStyle(Qt::DashDotDotLine);
  qp->setPen(pen);
  qp->drawLine(20, 200, 250, 200);

  QVector<qreal> dashes;
  qreal space = 4;

  dashes << 1 << space << 5 << space; pen.setStyle(Qt::CustomDashLine); pen.setDashPattern(dashes); qp->setPen(pen);
  qp->drawLine(20, 240, 250, 240);
}

Chúng ta vẽ 6 đường thẳng lên màn hình, mỗi đường thẳng vẽ một kiểu riêng.

void Lines::paintEvent(QPaintEvent *e) {
    
  Q_UNUSED(e);
  
  QPainter qp(this);
  drawLines(&qp);
}

Chúng ta override phương thức paintEvent() của lớp QWidget, phương thức này tự động được gọi khi các trạng thái của widget thay đổi, chẳng hạn như resize kích thước cửa sổ, ẩn/hiện widget… Bên trong phương thức chúng ta tạo đối tượng QPainter để vẽ.

QPen pen(Qt::black, 2, Qt::SolidLine);
qp->setPen(pen);

Chúng ta tạo đối tượng QPen. Tham số đầu tiên là màu vẽ, tham số thứ 2 là độ lớn của đường thẳng, tham số thứ 3 là loại đường thẳng, ở đây là Qt::SolidLine tức là đường thẳng bình thường. Bạn có thể tham khảo một số loại đường thẳng khác ở đây. Sau khi có QPen thì phải gắn nó vào với đối tượng QPainter bằng phương thức setPen(). Có thể hiểu QPainter là thợ vẽ, QPen là cây bút vẽ, thợ vẽ khi vẽ sẽ chọn loại bút để vẽ theo ý mình.

qp->drawLine(20, 40, 250, 40);

Phương thức drawLine()  vẽ một đường thẳng. Phương thức này nhận 4 tham số là tọa độ điểm bắt đầu và tọa độ điểm kết thúc của đường thẳng.

pen.setStyle(Qt::DashLine);

Phương thức setStyle() sẽ thay đổi loại đường thẳng mà chúng ta muốn.

#include <QApplication>
#include "lines.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  Lines window;
  
  window.resize(280, 270);
  window.setWindowTitle("Lines");
  window.show();

  return app.exec();
}
Capturea

Màu sắc

Màu sắc trong máy tính được biểu diễn bởi 3 giá trị là đỏ (Red), vàng (Yellow) và xanh nước biển (Blue) – RGB. Mỗi giá trị này nhận giá trị từ 0→255. Ví dụ dưới đây sẽ vẽ 9 hình chữ nhật với 9 màu khác nhau.

#pragma once 

#include <QWidget>

class Colours : public QWidget {
    
  public:
    Colours(QWidget *parent = 0);

  protected:
    void paintEvent(QPaintEvent *e);
    
  private:
    void doPainting();  
};
#include <QPainter>
#include "colours.h"

Colours::Colours(QWidget *parent)
    : QWidget(parent)
{ }

void Colours::paintEvent(QPaintEvent *e) {
    
  Q_UNUSED(e);  
  
  doPainting();
}

void Colours::doPainting() {
    
  QPainter painter(this);
  painter.setPen(QColor("#d4d4d4"));

  painter.setBrush(QBrush("#c56c00"));
  painter.drawRect(10, 15, 90, 60);

  painter.setBrush(QBrush("#1ac500"));
  painter.drawRect(130, 15, 90, 60);

  painter.setBrush(QBrush("#539e47"));
  painter.drawRect(250, 15, 90, 60);
  
  painter.setBrush(QBrush("#004fc5"));
  painter.drawRect(10, 105, 90, 60);

  painter.setBrush(QBrush("#c50024"));
  painter.drawRect(130, 105, 90, 60);

  painter.setBrush(QBrush("#9e4757"));
  painter.drawRect(250, 105, 90, 60);

  painter.setBrush(QBrush("#5f3b00"));
  painter.drawRect(10, 195, 90, 60);

  painter.setBrush(QBrush("#4c4c4c"));
  painter.drawRect(130, 195, 90, 60);

  painter.setBrush(QBrush("#785f36"));
  painter.drawRect(250, 195, 90, 60);
}

Chúng ta vẽ 9 hình chữ nhật có 9 màu khác nhau.

painter.setBrush(QBrush("#c56c00"));
painter.drawRect(10, 15, 90, 60);

Lớp QBrush quy định kiểu vẽ các đối tượng hình học được vẽ bởi lớp QPainter. Phương thức drawRect() của lớp QPainter vẽ một hình chữ nhật với 4 tham số là tọa độ x-y của góc trái-trên và tọa độ x-y của góc phải-dưới của hình chữ nhật.

#include <QApplication>
#include "colours.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  Colours window;
  
  window.resize(360, 280);
  window.setWindowTitle("Colours");
  window.show();
  
  return app.exec();
}
Capture

Pattern

Ví dụ dưới đây cũng sẽ vẽ các hình chữ nhật như ví dụ ở trên, chỉ khác là chúng ta sẽ khai thác các kiểu vẽ khác nhau có trong Qt. Trong ví dụ trên thì chúng ta cho vẽ mặc định theo kiểu tô màu toàn bộ phần bên trong hình chữ nhật.

#pragma once

#include <QWidget>

class Patterns : public QWidget {
    
  public:
    Patterns(QWidget *parent = 0);

  protected:
    void paintEvent(QPaintEvent *e);

  private:
    void doPainting();
};
#include <QApplication>
#include <QPainter>
#include "patterns.h"

Patterns::Patterns(QWidget *parent)
    : QWidget(parent)
{ }

void Patterns::paintEvent(QPaintEvent *e) {
    
  Q_UNUSED(e);  
  
  doPainting();
}

void Patterns::doPainting() {
    
  QPainter painter(this);
  painter.setPen(Qt::NoPen);

  painter.setBrush(Qt::HorPattern);
  painter.drawRect(10, 15, 90, 60);

  painter.setBrush(Qt::VerPattern);
  painter.drawRect(130, 15, 90, 60);

  painter.setBrush(Qt::CrossPattern);
  painter.drawRect(250, 15, 90, 60);
  
  painter.setBrush(Qt::Dense7Pattern);
  painter.drawRect(10, 105, 90, 60);

  painter.setBrush(Qt::Dense6Pattern);
  painter.drawRect(130, 105, 90, 60);

  painter.setBrush(Qt::Dense5Pattern);
  painter.drawRect(250, 105, 90, 60);

  painter.setBrush(Qt::BDiagPattern);
  painter.drawRect(10, 195, 90, 60);

  painter.setBrush(Qt::FDiagPattern);
  painter.drawRect(130, 195, 90, 60);

  painter.setBrush(Qt::DiagCrossPattern);
  painter.drawRect(250, 195, 90, 60);
}

Chúng ta vẽ 9 hình chữ nhật với 9 kiểu vẽ khác nhau.

#include <QDesktopWidget>
#include <QApplication>
#include "patterns.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  Patterns window;
  
  window.resize(350, 280);
  window.setWindowTitle("Patterns");
  window.show();

  return app.exec();
}
Capture

Độ trong suốt – Transparency

Trong đồ họa máy tính, độ trong suốt được định nghĩa bằng giá trị alpha, giá trị này được máy tính định nghĩa bằng cách kết hợp giá trị màu RGB tại một tọa độ với giá trị màu nền để cho ra độ trong suốt.

#pragma once

#include <QWidget>

class TransparentRectangles : public QWidget {

  public:
    TransparentRectangles(QWidget *parent = 0);

  protected:
    void paintEvent(QPaintEvent *event);
    void doPainting();
};
#include <QApplication>
#include <QPainter>
#include <QPainterPath>
#include "transparent_rectangles.h"

TransparentRectangles::TransparentRectangles(QWidget *parent)
    : QWidget(parent)
{ }

void TransparentRectangles::paintEvent(QPaintEvent *e) {
    
  Q_UNUSED(e);
  
  doPainting();
}

void TransparentRectangles::doPainting() {
    
  QPainter painter(this);
  
  for (int i=1; i<=11; i++) {
    painter.setOpacity(i*0.1);
    painter.fillRect(50*i, 20, 40, 40, Qt::darkGray);
  }    
}

Ví dụ trên vẽ 10 hình chữ nhật với độ trong suốt từ 0.0→1.0.

painter.setOpacity(i*0.1);

Phương thức setOpacity() quy định độ trong suốt, có giá trị từ 0.0 đến 1.0. Trong đó 0.0 là không nhìn thấy gì còn 1.0 là rõ hoàn toàn.

#include <QDesktopWidget>
#include <QApplication>
#include "transparent_rectangles.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  TransparentRectangles window;

  window.resize(630, 90);  
  window.setWindowTitle("Transparent rectangles");
  window.show();

  return app.exec();
}

Capture

Shape

Thư viện của Qt có thể vẽ được rất nhiều đối tượng hình học (shape) khác nhau.

#pragma once

#include <QWidget>

class Shapes : public QWidget {
    
  public:
    Shapes(QWidget *parent = 0);

  protected:
    void paintEvent(QPaintEvent *e);

  private:
    void doPainting();
};
#include <QApplication>
#include <QPainter>
#include <QPainterPath>
#include "shapes.h"

Shapes::Shapes(QWidget *parent)
    : QWidget(parent)
{ }

void Shapes::paintEvent(QPaintEvent *e) {
    
  Q_UNUSED(e);

  doPainting();
}

void Shapes::doPainting() {
  
  QPainter painter(this);

  painter.setRenderHint(QPainter::Antialiasing);
  painter.setPen(QPen(QBrush("#888"), 1));
  painter.setBrush(QBrush(QColor("#888")));

  QPainterPath path1;

  path1.moveTo(5, 5);
  path1.cubicTo(40, 5,  50, 50,  99, 99);
  path1.cubicTo(5, 99,  50, 50,  5, 5);
  painter.drawPath(path1);  

  painter.drawPie(130, 20, 90, 60, 30*16, 120*16);
  painter.drawChord(240, 30, 90, 60, 0, 16*180);
  painter.drawRoundRect(20, 120, 80, 50);

  QPolygon polygon({QPoint(130, 140), QPoint(180, 170), QPoint(180, 140),
      QPoint(220, 110), QPoint(140, 100)});

  painter.drawPolygon(polygon);

  painter.drawRect(250, 110, 60, 60);

  QPointF baseline(20, 250);
  QFont font("Georgia", 55);
  QPainterPath path2;
  path2.addText(baseline, font, "Q");
  painter.drawPath(path2);

  painter.drawEllipse(140, 200, 60, 60);
  painter.drawEllipse(240, 200, 90, 60);
}

Ví dụ trên vẽ 9 hình khác nhau.

QPainterPath path1;

path1.moveTo(5, 5);
path1.cubicTo(40, 5,  50, 50,  99, 99);
path1.cubicTo(5, 99,  50, 50,  5, 5);
painter.drawPath(path1);

Lớp QPainterPath là lớp được dùng để vẽ các hình phức tạp. Phương thức cubicTo(x1, y1, x2, y2, endPointX, endPointY) của lớp này sẽ vẽ đường cong Bezier từ vị trí hiện tại (mặc định hiện tại là tọa độ {0,0}) đến vị trí endPoint, các điểm được sử dụng để điều khiển quy trình vẽ và {x1, y1}{x2, y2}. Các bạn có thể tham khảo wiki về thuật toán vẽ đường cong Bezier.

painter.drawPie(130, 20, 90, 60, 30*16, 120*16);
painter.drawChord(240, 30, 90, 60, 0, 16*180);
painter.drawRoundRect(20, 120, 80, 50);

Phương thức drawPie(x, y, width, height, startAngle, spanAngle) và  drawChord(x, y, width, height, startAngle, spanAngle) vẽ hình tròn hoặc một phần của hình tròn bên trong một hình chữ nhật có vị trí {x, y} với chiều dài-rộng là {width, height}. Tham số startAngle spanAngle nhận giá trị là (1/16 * số đo một góc), startAngle là góc xoay, còn spanAngle là góc giữa hai dây cung, ví dụ nếu spanAngle là 16 * 360 thì khi đó ta được một hình tròn nguyên vẹn, 16 * 180 là nửa hình tròn, 16 * 120 sẽ ra hình quạt. Các bạn có thể thay đổi 2 tham số này để thấy sự thay đổi.

QPolygon polygon({QPoint(130, 140), QPoint(180, 170), QPoint(180, 140),
    QPoint(220, 110), QPoint(140, 100)});

painter.drawPolygon(polygon);

Phương thức drawPolygon() được dùng để vẽ một đa giác bất kỳ dùng tập hợp điểm trong lớp QPolygon. Ở ví dụ trên chúng ta có 5 điểm.

QPointF baseline(20, 250);
QFont font("Georgia", 55);
QPainterPath path2;
path2.addText(baseline, font, "Q");
painter.drawPath(path2);

Qt còn cho phép chúng ta vẽ hình dựa theo ký tự.

painter.drawEllipse(140, 200, 60, 60);
painter.drawEllipse(240, 200, 90, 60);

Phương thức drawEllipse() vẽ hình elip và có thể cả hình tròn nếu muốn. Tham số đầu vào là tọa độ {x, y} và chiều dài-rộng của một hình chữ nhật, Qt sẽ vẽ elip bên trong hình chữ nhật đó.

#include <QDesktopWidget>
#include <QApplication>
#include "shapes.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  Shapes window;

  window.resize(350, 280);  
  window.setWindowTitle("Shapes");
  window.show();

  return app.exec();
}

Capture

Gradient

Gradient là dải màu từ màu này đến màu khác. Có thể ứng dụng để làm mô phỏng bóng tối và ánh sáng. Trong ví dụ này chúng ta dùng linear gradient.

#pragma once

#include <QWidget>

class LinearGradients : public QWidget {

  public:
    LinearGradients(QWidget *parent = 0);

  protected:
    void paintEvent(QPaintEvent *e);
    
  private:
    void doPainting();  
};
#include <QApplication>
#include <QPainter>
#include "linear_gradients.h"

LinearGradients::LinearGradients(QWidget *parent)
    : QWidget(parent)
{ }

void LinearGradients::paintEvent(QPaintEvent *e) {
    
  Q_UNUSED(e);
  
  doPainting();
}  

void LinearGradients::doPainting() {
         
  QPainter painter(this);
  
  QLinearGradient grad1(0, 20, 0, 110);

  grad1.setColorAt(0.1, Qt::black);
  grad1.setColorAt(0.5, Qt::yellow);
  grad1.setColorAt(0.9, Qt::black);

  painter.fillRect(20, 20, 300, 90, grad1);

  QLinearGradient grad2(0, 55, 250, 0);

  grad2.setColorAt(0.2, Qt::black);
  grad2.setColorAt(0.5, Qt::red);
  grad2.setColorAt(0.8, Qt::black);

  painter.fillRect(20, 140, 300, 100, grad2);
}

Chúng ta vẽ 2 hình chữ nhật và tô màu gradient cho chúng.

QLinearGradient grad1(0, 20, 0, 110);

Lớp QLinearGradient tạo ra dải màu giữa 2 điểm.

grad1.setColorAt(0.1, Qt::black);
grad1.setColorAt(0.5, Qt::yellow);
grad1.setColorAt(0.9, Qt::black);

Dải màu trong gradient được tô giữa các điểm dừng. Bạn có thể điều chỉnh các tham số để thấy sự thay đổi.

painter.fillRect(20, 20, 300, 90, grad1);

Tô màu hình chữ nhật với gradient.

#include <QApplication>
#include "linear_gradients.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  LinearGradients window;

  window.resize(350, 260);  
  window.setWindowTitle("Linear gradients");
  window.show();

  return app.exec();
}
Capture

Radial gradient

Radial gradient là dải màu giữa hai hình tròn.

#pragma once

#include <QWidget>

class RadialGradient : public QWidget {

  public:
    RadialGradient(QWidget *parent = 0);

  protected:
    void paintEvent(QPaintEvent *e);
    
  private:
    void doPainting();  
};
#include <QApplication>
#include <QPainter>
#include "radial_gradient.h"

RadialGradient::RadialGradient(QWidget *parent)
    : QWidget(parent)
{ }

void RadialGradient::paintEvent(QPaintEvent *e) {
    
  Q_UNUSED(e);
  
  doPainting();
}

void RadialGradient::doPainting() {
  
  QPainter painter(this);
  
  int h = height();
  int w = width();

  QRadialGradient grad1(w/2, h/2, 80);

  grad1.setColorAt(0, QColor("#032E91"));
  grad1.setColorAt(0.3, Qt::white);
  grad1.setColorAt(1, QColor("#032E91"));

  painter.fillRect(0, 0, w, h, grad1);
}

Ví dụ trên tạo dải màu quanh hình tròn, dải màu này sẽ xuất phát từ tâm hình tròn.

#include <QApplication>
#include "radial_gradient.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  RadialGradient window;

  window.resize(300, 250);  
  window.setWindowTitle("Radial gradient");
  window.show();

  return app.exec();
}
Capture

Animation

Trong ví dụ cuối cùng chúng ta sẽ làm hiệu ứng chuyển động. Chúng ta sẽ cho một đoạn chữ xuất phát từ tâm màn hình với kích thước lớn dần dần, màu chữ cũng sẽ từ từ nhạt đi trong một khoảng thời gian.

#pragma once

#include <QWidget>

class Puff : public QWidget {

  public:
    Puff(QWidget *parent = 0);

  protected:
    void paintEvent(QPaintEvent *event);
    void timerEvent(QTimerEvent *event);

  private:
    int x;
    qreal opacity;
    int timerId;
    
    void doPainting();
};

Chúng ta override hai phương thức paintEvent()timerEvent().

#include <QPainter>
#include <QTimer>
#include <QTextStream>
#include "puff.h"

Puff::Puff(QWidget *parent)
    : QWidget(parent) {
        
  x = 1;
  opacity = 1.0;
  timerId = startTimer(15);
}

void Puff::paintEvent(QPaintEvent *e) {
    
  Q_UNUSED(e);  
  
  doPainting();
}

void Puff::doPainting() {
  
  QPainter painter(this);
  QTextStream out(stdout);

  QString text = "Pho Code";

  painter.setPen(QPen(QBrush("#575555"), 1));

  QFont font("Courier", x, QFont::DemiBold);
  QFontMetrics fm(font);
  int textWidth = fm.width(text);

  painter.setFont(font);

  if (x < 10) { opacity -= 0.01; painter.setOpacity(opacity); } if (opacity >= 0) {
    killTimer(timerId);
    out << "timer stopped" << endl;
  }

  int h = height();
  int w = width();

  painter.translate(QPoint(w/2, h/2));
  painter.drawText(-textWidth/2, 0, text);
}

void Puff::timerEvent(QTimerEvent *e) {
    
  Q_UNUSED(e);
  
  x += 1;
  repaint();
}
Puff::Puff(QWidget *parent)
    : QWidget(parent) {
        
  x = 1;
  opacity = 1.0;
  timerId = startTimer(15);
}

Chúng ta cho chạy timer với độ trễ là 15ms.

void Puff::timerEvent(QTimerEvent *e) {
    
  Q_UNUSED(e);
  
  x += 1;
  repaint();
}

Cứ sau 15ms, chúng ta sẽ cho tăng kích thước chữ và gọi phương thức repaint().

if (x > 10) {
  opacity -= 0.01;
  painter.setOpacity(opacity);
}

Nếu kích thước chữ lớn hơn 10, chúng ta bắt đầu làm mờ chữ.

if (opacity <= 0) {
  killTimer(timerId);
  out << "timer stopped" << endl;
}

Khi độ trong suốt của chữ đã biến mất hoàn toàn thì chúng ta dừng timer.

#include <QApplication>
#include "puff.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv); 

  Puff window;

  window.resize(350, 280);
  window.setWindowTitle("Puff");
  window.show();

  return app.exec();
}

Untitled

Qt 5 C++ – Widget phần 2

Trong phần này chúng ta tiếp tục tìm hiểu về một số widget hay dùng trong Qt 5. Gồm có QCheckBox, QListWidget, QProgressBar, QPixmap, QSplitter, và QTableWidget.

QCheckBox

CheckBox thi chắc các bạn cũng thấy nhiều rồi, còn nếu chưa thì làm theo ví dụ dưới đây rồi thấy 🙂

Trong ví dụ này, chúng ta cho hiển thị một checkbox, nếu tick vào checkbox thì thay đổi tiêu đề cửa sổ.

#pragma once

#include <QWidget>

class CheckBox : public QWidget {
    
  Q_OBJECT

  public:
    CheckBox(QWidget *parent = 0);

  private slots:
    void showTitle(int);
};
#include <QCheckBox>
#include <QHBoxLayout>
#include "checkbox.h"

CheckBox::CheckBox(QWidget *parent)
    : QWidget(parent) {

  QHBoxLayout *hbox = new QHBoxLayout(this);
  
  QCheckBox *cb = new QCheckBox("Show Title", this);
  cb->setCheckState(Qt::Checked);
  hbox->addWidget(cb, 0, Qt::AlignLeft | Qt::AlignTop);

  connect(cb, &QCheckBox::stateChanged, this, &CheckBox::showTitle);
}

void CheckBox::showTitle(int state) {
    
  if (state == Qt::Checked) {
    setWindowTitle("QCheckBox");
  } else {
    setWindowTitle(" ");
  }
}

Chúng ta cho hiển thị checkbox rồi kết nối signal stateChanged() của nó với slot showTitle().

cb->setCheckState(Qt::Checked);

Mặc định checkbox sẽ được tick khi chương trình chạy.

void CheckBox::showTitle(int state) {

  if (state == Qt::Checked) {
    setWindowTitle("QCheckBox");
  } else {
    setWindowTitle(" ");
  }
}

Chúng ta có thể kiểm tra checkbox đã được tick hay chưa bằng cách so sánh nó với macro Qt::Checked hoặc có thể dùng phương thức QCheckBox::isChecked().

#include <QApplication>
#include "checkbox.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  CheckBox window;

  window.resize(250, 150);
  window.setWindowTitle("QCheckBox");
  window.show();

  return app.exec();
}
Capture

QListWidget

QListWidget dùng để hiển thị một danh sách các item, trong ví dụ dưới đây chúng ta sẽ cài đặt các phương thức add, rename, remove cho listwidget.

#pragma once

#include <QWidget>
#include <QPushButton>
#include <QListWidget>

class ListWidget : public QWidget {
    
  Q_OBJECT

  public:
    ListWidget(QWidget *parent = 0);

  private slots:
    void addItem();
    void renameItem();
    void removeItem();
    void clearItems();

  private:
    QListWidget *lw;
    QPushButton *add;
    QPushButton *rename;
    QPushButton *remove;
    QPushButton *removeAll; 
};
#include "listwidget.h"
#include <QVBoxLayout>
#include <QInputDialog>

ListWidget::ListWidget(QWidget *parent)
    : QWidget(parent) {

  QVBoxLayout *vbox = new QVBoxLayout();
  vbox->setSpacing(10);

  QHBoxLayout *hbox = new QHBoxLayout(this);

  lw = new QListWidget(this);
  lw->addItem("The Omen"); 
  lw->addItem("The Exorcist");
  lw->addItem("Notes on a scandal");
  lw->addItem("Fargo");
  lw->addItem("Capote");

  add = new QPushButton("Add", this);
  rename = new QPushButton("Rename", this);
  remove = new QPushButton("Remove", this);
  removeAll = new QPushButton("Remove All", this);

  vbox->setSpacing(3);
  vbox->addStretch(1);
  vbox->addWidget(add);
  vbox->addWidget(rename);
  vbox->addWidget(remove);
  vbox->addWidget(removeAll);
  vbox->addStretch(1);

  hbox->addWidget(lw);
  hbox->addSpacing(15);
  hbox->addLayout(vbox);
  
  connect(add, &QPushButton::clicked, this, &ListWidget::addItem);
  connect(rename, &QPushButton::clicked, this, &ListWidget::renameItem);
  connect(remove, &QPushButton::clicked, this, &ListWidget::removeItem);
  connect(removeAll, &QPushButton::clicked, this, &ListWidget::clearItems);
  
  setLayout(hbox);
}

void ListWidget::addItem() {
    
  QString c_text = QInputDialog::getText(this, "Item", "Enter new item");
  QString s_text = c_text.simplified();
  
  if (!s_text.isEmpty()) {
      
    lw->addItem(s_text);
    int r = lw->count() - 1;
    lw->setCurrentRow(r);
  }
}

void ListWidget::renameItem() {
    
  QListWidgetItem *curitem = lw->currentItem();
  
  QString c_text = curitem->text();
  QString r_text = QInputDialog::getText(this, "Item", "Enter new item", 
                                         QLineEdit::Normal, c_text);
  
  QString s_text = r_text.simplified();
  curitem.setText(s_text);
}

void ListWidget::removeItem() {
    
  int r = lw->currentRow();

  if (r != -1) {
      
    QListWidgetItem *item = lw->takeItem(r);
    lw->removeItemWidget(item);
  }
}

void ListWidget::clearItems(){
    
  if (lw->count() != 0) {
    lw->clear();
  }
}

Chúng ta cho hiển thị listwidget và bốn button. Những button này sẽ được dùng để add, rename, remove item trong list.

lw = new QListWidget(this);
lw->addItem("The Omen"); 
lw->addItem("The Exorcist");
lw->addItem("Notes on a scandal");
lw->addItem("Fargo");
lw->addItem("Capote);

Tạo QListWidget và đưa 5 item vào.

void ListWidget::addItem() {
    
  QString c_text = QInputDialog::getText(this, "Item", "Enter new item");
  QString s_text = c_text.simplified();
  
  if (!s_text.isEmpty()) {
      
    lw->addItem(s_text);
    int r = lw->count() - 1;
    lw->setCurrentRow(r);
  }
}

Phương thức addItem() sẽ thêm một item vào list widget bằng cách hiển thị một dialog (hộp thoại) để người dùng nhập tên của item mới vào. Dialog này sẽ trả về một string là tên của item mới, sau đó chúng ta dùng phương thức simplified() để bỏ các khoảng trắng thừa rồi kiểm tra nếu tên item không phải là chuỗi rỗng thì chúng ta thêm item này vào list. Cuối cùng dùng phương thức setCurrentRow() để làm nổi bật item đó trên list.

void ListWidget::renameItem() {
    
  QListWidgetItem *curitem = lw->currentItem();  
  QString c_text = curitem->text();
  QString r_text = QInputDialog::getText(this, "Item", "Enter new item", 
                                         QLineEdit::Normal, c_text);
  
  QString s_text = r_text.simplified();
  curitem.setText(s_text);  
}

Để đổi tên một item trong list thì đầu tiên chúng ta lấy biến tham chiếu đến item đang được chọn trong list bằng phương thức currentItem(). Tiếp theo chúng ta hiển thị một dialog để người dùng nhập tên mới của item, trước đó chúng ta cũng lấy tên cũ của item để đưa vào dialog đề phòng trường hợp người dùng đổi ý không muốn đổi tên item nữa. Dialog này sẽ trả về một string và chúng ta cũng dùng phương thức simplified() để bỏ các khoảng trắng thừa. Sau đó dùng phương thức setText() để đổi tên item.

void ListWidget::removeItem() {
    
  int r = lw->currentRow();

  if (r != -1) {
      
    QListWidgetItem *item = lw->takeItem(r);
    lw->removeItemWidget(item);
  }
}

Để xóa một item thì chúng ta lấy số hàng đang được chọn trong list bằng phương thức currentRow(), sau đó lấy biến tham chiếu đến item bằng phương thức takeItem()phương thức này sẽ trả về một đối tượng QListWidgetItem, cuối cùng chúng ta dùng phương thức removeItemWidget() để xóa item được chọn ra.

void ListWidget::clearItems(){
    
  if (lw->count() != 0) {
    lw->clear();
  }
}

Phương thức clear() dùng để xóa toàn bộ item ra khỏi list.

#include <QApplication>
#include "listwidget.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  ListWidget window;

  window.setWindowTitle("QListWidget");
  window.show();
  
  return app.exec();
}

Capture

QProgressBar

QProgressBar dùng để theo dõi tiến trình hoàn thành của một công việc nào đó.
#pragma once

#include <QWidget>
#include <QProgressBar>
#include <QPushButton>

class ProgressBarEx : public QWidget {
    
  Q_OBJECT
  
  public:
    ProgressBarEx(QWidget *parent = 0);

  private:
    int progress;    
    QTimer *timer;
    QProgressBar *pbar; 
    QPushButton *startBtn;
    QPushButton *stopBtn;
    static const int DELAY = 200;
    static const int MAX_VALUE = 100;
    
    void updateBar();
    void startMyTimer();
    void stopMyTimer();
};
#include <QProgressBar>
#include <QTimer>
#include <QGridLayout>
#include "progressbar.h"

ProgressBarEx::ProgressBarEx(QWidget *parent)
    : QWidget(parent) {
        
  progress = 0;      
  timer = new QTimer(this);
  connect(timer, &QTimer::timeout, this, &ProgressBarEx::updateBar);

  QGridLayout *grid = new QGridLayout(this);
  grid->setColumnStretch(2, 1);
         
  pbar = new QProgressBar();
  grid->addWidget(pbar, 0, 0, 1, 3);

  startBtn = new QPushButton("Start", this);
  connect(startBtn, &QPushButton::clicked, this, &ProgressBarEx::startMyTimer);
  grid->addWidget(startBtn, 1, 0, 1, 1);
  
  stopBtn = new QPushButton("Stop", this);
  connect(stopBtn, &QPushButton::clicked, this, &ProgressBarEx::stopMyTimer);
  grid->addWidget(stopBtn, 1, 1);
}

void ProgressBarEx::startMyTimer() {
  
  if (progress >= MAX_VALUE) {
      
      progress = 0;
      pbar->setValue(0);
  }
    
  if (!timer->isActive()) {
      
    startBtn->setEnabled(false); 
    stopBtn->setEnabled(true); 
    timer->start(DELAY);
  }
}

void ProgressBarEx::stopMyTimer() {
    
  if (timer->isActive()) {
      
    startBtn->setEnabled(true);  
    stopBtn->setEnabled(false);
    timer->stop();
  }
}

void ProgressBarEx::updateBar() {
    
  progress++;
  
  if (progress <= MAX_VALUE) { pbar->setValue(progress);
  } else {
      
    timer->stop();
    startBtn->setEnabled(true);  
    stopBtn->setEnabled(false);      
  }
}

Trong ví dụ trên, chúng ta có một  QProgressBar và hai button. Một button để chạy timer, khi timer chạy, nó sẽ update progress bar. Button còn lại dùng để tắt timer.

pbar = new QProgressBar();

Khi khởi tạo QProgressBar, phạm vi giá trị mặc định là từ 0 đến 100.

void ProgressBarEx::updateBar() {
  progress++;
  
  if (progress <= MAX_VALUE) { pbar->setValue(progress);
  } else {
      
    timer->stop();
    startBtn->setEnabled(true);  
    stopBtn->setEnabled(false);      
  }
}

Thông tin về tiến trình được lưu trong biến progress. Phương thức setValue() sẽ cập nhật giá trị cho progress bar.

#include <QApplication>
#include "progressbar.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  ProgressBarEx window;

  window.resize(250, 150);
  window.setWindowTitle("QProgressBar");
  window.show();

  return app.exec();
}
Capture

QPixmap

QPixmap là lớp dùng để làm việc với ảnh.

#pragma once

#include <QWidget>

class Pixmap : public QWidget {
    
  public:
    Pixmap(QWidget *parent = 0);
};
#include <QApplication>
#include <QPixmap>
#include <QLabel>
#include <QHBoxLayout>
#include "pixmap.h"

Pixmap::Pixmap(QWidget *parent)
    : QWidget(parent) {

  QHBoxLayout *hbox = new QHBoxLayout(this);
  
  QPixmap pixmap(qApp->applicationDirPath() + "/qt.png");
  
  QLabel *label = new QLabel(this);
  label->setPixmap(pixmap);

  hbox->addWidget(label, 0, Qt::AlignTop);
}

Trong ví dụ trên chúng ta cho hiển thị logo của Qt lên pixmap.

QPixmap pixmap(qApp->applicationDirPath() + "/qt.jpg");

QLabel *label = new QLabel(this);
label->setPixmap(pixmap);

Chúng ta không thể đưa pixmap trực tiếp lên window mà phải đưa vào một QLabel rồi hiển thị từ label này.

#include <QApplication>
#include "pixmap.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  Pixmap window;

  window.setWindowTitle("QPixmap");
  window.show();
  
  return app.exec();
}

Capture

QSplitter

QSplitter cho phép chúng ta thay đổi kích thước của các widget con bằng cách kéo đường viền của chúng.

#pragma once

#include <QWidget>

class Splitter : public QWidget {
    
  public:
    Splitter(QWidget *parent = 0);
};
#include <QFrame>
#include <QSplitter>
#include <QHBoxLayout>
#include "splitter.h"

Splitter::Splitter(QWidget *parent)
    : QWidget(parent) {
        
  QHBoxLayout *hbox = new QHBoxLayout(this);

  QFrame *topleft = new QFrame(this);
  topleft->setFrameShape(QFrame::StyledPanel);

  QFrame *topright = new QFrame(this);
  topright->setFrameShape(QFrame::StyledPanel);

  QSplitter *splitter1 = new QSplitter(Qt::Horizontal, this);
  splitter1->addWidget(topleft);
  splitter1->addWidget(topright);

  QFrame *bottom = new QFrame(this);
  bottom->setFrameShape(QFrame::StyledPanel);

  QSplitter *splitter2 = new QSplitter(Qt::Vertical, this);
  splitter2->addWidget(splitter1);
  splitter2->addWidget(bottom);
  
  QList<int> sizes({50, 100});
  splitter2->setSizes(sizes);

  hbox->addWidget(splitter2);
}

Trong ví dụ này chúng ta dùng ba frame widget và hai splitter widget.

QSplitter *splitter1 = new QSplitter(Qt::Horizontal, this);
splitter1->addWidget(topleft);
splitter1->addWidget(topright);

Tạo splitter và thêm hai frame vào.

QSplitter *splitter2 = new QSplitter(Qt::Vertical, this);
splitter2->addWidget(splitter1);

Chúng ta có thể chồng các splitter lên nhau.

QList<int> sizes({50, 100});
splitter2->setSizes(sizes);

Phương thức setSizes() điều chỉnh kích thước của các widget con trong splitter.

#include <QDesktopWidget>
#include <QApplication>
#include "splitter.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  Splitter window;

  window.resize(350, 300);
  window.setWindowTitle("QSplitter");
  window.show();
  
  return app.exec();
}
Untitled

QTableWidget

QTableWidget là widget tạo bảng tính giống như Microsoft Excel. Widget này sử dụng khá phức tạp, các bạn có thể tự tìm hiểu thêm.

#pragma once

#include <QWidget>

class Table : public QWidget {
    
  public:
    Table(QWidget *parent = 0);
};
#include <QHBoxLayout>
#include <QTableWidget>
#include "table.h"

Table::Table(QWidget *parent)
    : QWidget(parent) {
        
  QHBoxLayout *hbox = new QHBoxLayout(this);

  QTableWidget *table = new QTableWidget(25, 25, this);

  hbox->addWidget(table);
}

Ví dụ trên sẽ hiển thị QTableWidget lên window.

QTableWidget *table = new QTableWidget(25, 25, this);

Chúng ta tạo bảng có 25 dòng và 25 cột.

#include <QApplication>
#include "table.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  Table window;

  window.resize(400, 250);
  window.setWindowTitle("QTableWidget");
  window.show();

  return app.exec();
}
Capture

Qt 5 C++ – Widgets phần 1

Trong phần này chúng ta sẽ nói về một số widget cơ bản trong Qt 5 gồm có QLabel, QSlider, QComboBox, QSpinBox, QLineEdit, và QMainWindow.

Widget là các phần tử cơ bản để xây dựng nên một ứng dụng GUI.  Hệ thống widget của Qt 5 rất đa dạng.

QLabel

QLabel được dùng để hiển thị văn bản và hình ảnh. Nhưng không thể tương tác với người dùng.

#pragma once

#include <QWidget>
#include <QLabel>

class Label : public QWidget {

  public:
    Label(QWidget *parent = 0);

  private:
    QLabel *label;
};
#include <QVBoxLayout>
#include <QFont>
#include "label.h"

Label::Label(QWidget *parent)
    : QWidget(parent) {

  QString lyrics = "Who doesn't long for someone to hold\n\
Who knows how to love you without being told\n\
Somebody tell me why I'm on my own\n\
If there's a soulmate for everyone\n\
\n\
Here we are again, circles never end\n\
How do I find the perfect fit\n\
There's enough for everyone\n\
But I'm still waiting in line\n\
\n\
Who doesn't long for someone to hold\n\
Who knows how to love you without being told\n\
Somebody tell me why I'm on my own\n\
If there's a soulmate for everyone";

  label = new QLabel(lyrics, this);
  label->setFont(QFont("Comic San MS"));

  QVBoxLayout *vbox = new QVBoxLayout();
  vbox->addWidget(label);
  setLayout(vbox);
}

Ví dụ trên in ra lời bài hát bằng QLabel.

label = new QLabel(lyrics, this);
label->setFont(QFont("Comic San MS"));

Tạo đối tượng QLabel và thiết lập kiểu font chữ.

#include <QApplication>
#include <QTextStream>
#include "label.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  Label window;

  window.setWindowTitle("QLabel");
  window.show();

  return app.exec();
}
Capture

QSlider

QSlider là widget hiển thị một thanh trượt có thể kéo qua kéo lại. Thường dùng để thiết lập giá trị cho một công việc nào đó.

#pragma once

#include <QWidget>
#include <QSlider>
#include <QLabel>

class Slider : public QWidget {
    
  Q_OBJECT
  
  public:
    Slider(QWidget *parent = 0);

  private:
    QSlider *slider; 
    QLabel *label;
};
#include <QHBoxLayout>
#include "slider.h"

Slider::Slider(QWidget *parent)
    : QWidget(parent) {

  QHBoxLayout *hbox = new QHBoxLayout(this);
         
  slider = new QSlider(Qt::Horizontal , this);
  hbox->addWidget(slider);

  label = new QLabel("0", this);
  hbox->addWidget(label);

  connect(slider, &QSlider::valueChanged, label, 
    static_cast<void (QLabel::*)(int)>(&QLabel::setNum));
}

Chúng ta cho hiển thị hai widget, một slider và một label. Slider sẽ cập nhật con số được hiển thị trên label.

slider = new QSlider(Qt::Horizontal , this);

Tạo một đối tượng QSlider.

connect(slider, &QSlider::valueChanged, label, 
  static_cast<void (QLabel::*)(int)>(&QLabel::setNum));

Chúng ta kết nối signal valueChanged() của QSlider đến slot (phương thức) setNum().  Bản thân phương thức setNum() là một phương thức đã được overload, tức là phương thức này có nhiều prototype, mỗi prototype xử lý một kiểu dữ liệu khác nhau (hiện tại setNum() Qt 5.5.1 có hai prototype là int và double) nên chúng ta phải dùng static_cast để chỉ định đúng phương thức cần dùng.

#include <QApplication>
#include "slider.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  Slider window;

  window.setWindowTitle("QSlider");
  window.show();

  return app.exec();
}
Capture

QComboBox

Lớp QComboBox hiển thị một danh sách các item mà user có thể chọn lựa.

#pragma once

#include <QWidget>
#include <QComboBox>
#include <QLabel>

class ComboBoxEx : public QWidget {
    
  Q_OBJECT
  
  public:
    ComboBoxEx(QWidget *parent = 0);

  private:
    QComboBox *combo; 
    QLabel *label;
};

Chúng ta sử dụng hai widget là combobox và label.

#include <QHBoxLayout>
#include "combobox.h"

ComboBoxEx::ComboBoxEx(QWidget *parent)
    : QWidget(parent) {
        
  QStringList distros = {"Arch", "Xubuntu", "Redhat", "Debian", 
      "Mandriva"};

  QHBoxLayout *hbox = new QHBoxLayout(this);
         
  combo = new QComboBox();
  combo->addItems(distros);
  
  hbox->addWidget(combo);
  hbox->addSpacing(15);

  label = new QLabel("Arch", this);
  hbox->addWidget(label);

  connect(combo, static_cast<void(QComboBox::*)(const QString &)>(&QComboBox::activated), 
      label, &QLabel::setText);
}

Trong ví dụ này, chúng ta hiển thị một combobox và item nào được chọn sẽ được hiển thị trên widget.

QStringList distros = {"Arch", "Xubuntu", "Redhat", "Debian", "Mandriva"};

Chúng ta dùng QStringList để lưu danh sách các chuỗi, ở đây là danh sách tên các phiên bản của hệ điều hành Linux.

combo = new QComboBox();
combo->addItems(distros);

Chúng ta tạo đối tượng QComboBox và thêm các item vào bằng phương thức addItems().

connect(combo, static_cast<void(QComboBox::*)(const QString &)>(&QComboBox::activated) label, &QLabel::setText);

Chúng ta kết nối signal activated() của lớp combobox với slot setText() của label. Cũng như ở trên, signal này cũng đã được overload nhiều prototype nên chúng ta phải dùng static_cast để chọn prototype phù hợp.

#include <QApplication>
#include "combobox.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  ComboBoxEx window;

  window.resize(300, 150);
  window.setWindowTitle("QComboBox");
  window.show();

  return app.exec();
}
Capture

QSpinBox

QSpinbox là widget chuyên dùng để làm việc với số nguyên và các giá trị rời rạc. Ví dụ dưới đây sẽ hiển thị một spinbox chứa các số trong tập từ 0→99. Số nào được chọn thì hiển thị lên label.

#pragma once

#include <QWidget>
#include <QSpinBox>

class SpinBox : public QWidget {
    
  Q_OBJECT

  public:
    SpinBox(QWidget *parent = 0);

  private:
    QSpinBox *spinbox;
};
#include <QHBoxLayout>
#include <QLabel>
#include "spinbox.h"

SpinBox::SpinBox(QWidget *parent)
    : QWidget(parent) {
        
  QHBoxLayout *hbox = new QHBoxLayout(this);   
  hbox->setSpacing(15);
    
  spinbox = new QSpinBox(this);
  QLabel *lbl = new QLabel("0", this);

  hbox->addWidget(spinbox);  
  hbox->addWidget(lbl);
  
  connect(spinbox, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), 
    lbl, static_cast<void (QLabel::*)(int)>(&QLabel::setNum));  
}

Chúng ta hiển thị spinbox lên window và kết nối signal valueChanged() vào slot setNum().

connect(spinbox, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), 
  lbl, static_cast<void (QLabel::*)(int)>(&QLabel::setNum)); 

Ở đây cả signal và slot đều được overloaded nên phải cast hai lần.

#include <QApplication>
#include "spinbox.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  SpinBox window;

  window.resize(250, 150);
  window.setWindowTitle("QSpinBox");
  window.show();

  return app.exec();
}
Capture

QLineEdit

QLineEdit là widget cho phép gõ một dòng văn bản. Xem ví dụ.

#pragma once

#include <QWidget>

class Ledit : public QWidget {
    
  public:
    Ledit(QWidget *parent = 0);
};
#include <QGridLayout>
#include <QLabel>
#include <QLineEdit>
#include "ledit.h"

Ledit::Ledit(QWidget *parent)
    : QWidget(parent) {
        
  QLabel *name = new QLabel("Name:", this);
  name->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
  QLabel *age = new QLabel("Age:", this);
  age->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
  QLabel *occupation = new QLabel("Occupation:", this);
  occupation->setAlignment(Qt::AlignRight | Qt::AlignVCenter);

  QLineEdit *le1 = new QLineEdit(this);
  QLineEdit *le2 = new QLineEdit(this);
  QLineEdit *le3 = new QLineEdit(this);

  QGridLayout *grid = new QGridLayout(); 

  grid->addWidget(name, 0, 0);
  grid->addWidget(le1, 0, 1);
  grid->addWidget(age, 1, 0);
  grid->addWidget(le2, 1, 1);
  grid->addWidget(occupation, 2, 0);
  grid->addWidget(le3, 2, 1);

  setLayout(grid);
}

Chúng ta hiển thị ba label và ba lineEdit. Các widget này sẽ được phân bố bằng QGridLayout.

#include "ledit.h"
#include <QApplication>

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  Ledit window;

  window.setWindowTitle("QLineEdit");
  window.show();

  return app.exec();
}
Capture

Statusbar

Statusbar là một khu vực trên ứng dụng dùng để hiển thị một số thông tin trạng thái cho người dùng.

Trong ví dụ dưới đây, chúng ta có hai button và một statusbar. Mỗi button sẽ hiển thị một thông tin trên status bar nếu được click. Trong Qt có hai cách để hiển thị statusbar, một là dùng lớp QStatusBar, hai là dùng statusbar có sẵn trong lớp QMainWindow

#pragma once

#include <QMainWindow>
#include <QPushButton>

class Statusbar : public QMainWindow {
    
  Q_OBJECT  

  public:
    Statusbar(QWidget *parent = 0);

  private slots:
    void OnOkPressed();
    void OnApplyPressed();

  private:
    QPushButton *okBtn;
    QPushButton *aplBtn;
};
#include <QLabel>
#include <QFrame>
#include <QStatusBar>
#include <QHBoxLayout>
#include "statusbar.h"

Statusbar::Statusbar(QWidget *parent)
    : QMainWindow(parent) {
        
  QFrame *frame = new QFrame(this);
  setCentralWidget(frame);
  
  QHBoxLayout *hbox = new QHBoxLayout(frame);

  okBtn = new QPushButton("OK", frame);
  hbox->addWidget(okBtn, 0, Qt::AlignLeft | Qt::AlignTop);

  aplBtn = new QPushButton("Apply", frame);
  hbox->addWidget(aplBtn, 1, Qt::AlignLeft | Qt::AlignTop);

  statusBar();

  connect(okBtn, &QPushButton::clicked, this, &Statusbar::OnOkPressed);
  connect(aplBtn, &QPushButton::clicked, this, &Statusbar::OnApplyPressed);
}

void Statusbar::OnOkPressed() {
    
  statusBar()->showMessage("OK button pressed", 2000);
}

void Statusbar::OnApplyPressed() {
    
 statusBar()->showMessage("Apply button pressed", 2000);
}
QFrame *frame = new QFrame(this);
setCentralWidget(frame);

Chúng ta tạo một đối tượng QFrame và đưa nó làm central widget của QMainWindow. Mỗi window chỉ chưa được một central widget.

okBtn = new QPushButton("OK", frame);
hbox->addWidget(okBtn, 0, Qt::AlignLeft | Qt::AlignTop);

aplBtn = new QPushButton("Apply", frame);
hbox->addWidget(aplBtn, 1, Qt::AlignLeft | Qt::AlignTop);

Chúng ta tạo ra hai button và đưa vào layout ngang.

statusBar();

Để hiển thị được statusbar thì chúng ta dùng phương thức statusBar().

void Statusbar::OnOkPressed() {
    
  statusBar()->showMessage("OK button pressed", 2000);
}

Phương thức showMessage() sẽ hiển thị thông điệp lên statusbar, tham số thứ 2 trong phưng thức này là thời gian mà thông điệp được hiển thị theo mili giây.

#include <QApplication>
#include "statusbar.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  Statusbar window;

  window.resize(300, 200);
  window.setWindowTitle("QStatusBar");
  window.show();
  
  return app.exec();
}
Capture

Qt 5 C++ – Event và signal

Event (sự kiện) là một phần quan trọng của bất kỳ ứng dụng GUI nào. Tất cả các ứng dụng dạng GUI xử lý event trong suốt thời gian mà nó chạy. Event phần lớn được tạo ra bởi người dùng, nhưng cũng có lúc được tạo ra bởi chính ứng dụng. Có ba thành phần tham gia vào hệ thống event:

  • Event nguồn
  • Đối tượng event
  • Event đích

Event nguồn là đối tượng tạo ra sự thay đổi. Cứ có gì đó trong ứng dụng tạo ra sự thay đổi nào đó thì nó chính là event nguồn. Đối tượng event là chính bản thân cái event đó đã được mã hóa. Event đích là đối tượng sẽ xử lý event đó.

Nếu bạn từng lập trình GUI bằng API của window, bạn đã biết là một ứng dụng GUI sau khi đã hoàn tất các công việc khởi tạo biến, đăng ký ID với hệ điều hành…v.v. thì ứng dụng đó sẽ bắt đầu một vòng lặp vô tận, vòng lặp này liên tục lắng nghe và bắt các event đang diễn ra và gửi nó đến đối tượng để xử lý bằng câu lệnh switch(). Trong Qt cũng vậy, cứ mỗi lần gọi phương thức exec() của ứng dụng thì ứng dụng sẽ bắt đầu vòng lặp của nó, tuy nhiên chúng ta không trực tiếp làm việc với vòng lặp này, mà thay vào đó chúng ta dùng hệ thống Signal & Slot của Qt, đây là một phần mở rộng của C++ và cũng là thành phần đặc trưng của riêng Qt dùng để kiểm soát event.

Nói sơ qua về cách hoạt động của cơ chế Signal & Slot, một signal (tiếng việt là tín hiệu) sẽ được phát ra khi có sự kiện nào đó diễn ra, slot là phương thức của một đối tượng nào đó, mà cứ mỗi khi chỉ có đúng signal đó được phát ra thì slot này sẽ được thực thi.

Click

Ví dụ dưới đây là một chương trình đơn giản để mô tả cách hoạt động của hệ thống event. Chương trình hiển thị một button, cứ mỗi lần click button này thì thoát chương trình.

#pragma once

#include <QWidget>

class Click : public QWidget {
    
  public:
    Click(QWidget *parent = 0);
};
#include <QPushButton>
#include <QApplication>
#include <QHBoxLayout>
#include "click.h"

Click::Click(QWidget *parent)
    : QWidget(parent) {
       
  QHBoxLayout *hbox = new QHBoxLayout(this);
  hbox->setSpacing(5);
        
  QPushButton *quitBtn = new QPushButton("Quit", this);
  hbox->addWidget(quitBtn, 0, Qt::AlignLeft | Qt::AlignTop);

  connect(quitBtn, &QPushButton::clicked, qApp, &QApplication::quit);
}
connect(quitBtn, &QPushButton::clicked, qApp, &QApplication::quit);

Phương thức connect() có tác dụng kết nối signal của đối tượng này tới slot của đối tượng khác. Trong trường hợp này nó kết nối signal clicked của button tới slot quit của qApp. 

#include <QApplication>
#include "click.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  Click window;
  
  window.resize(250, 150);
  window.setWindowTitle("Click");
  window.show();

  return app.exec();
}
Capture

Tương tác với bàn phím – KeyPress

Ta sẽ học cách tương tác với bàn phím qua ví dụ dưới đây.

#pragma once

#include <QWidget>

class KeyPress : public QWidget {

  public:
    KeyPress(QWidget *parent = 0);

  protected:
    void keyPressEvent(QKeyEvent * e);
};
#include <QApplication>
#include <QKeyEvent>
#include "keypress.h"

KeyPress::KeyPress(QWidget *parent)
    : QWidget(parent)
{ }

void KeyPress::keyPressEvent(QKeyEvent *event) {

   if (event->key() == Qt::Key_Escape) {  
       qApp->quit();
   } 
}

Thoát chương trình khi nhấn phím Escape.

void KeyPress::keyPressEvent(QKeyEvent *e) {

   if (e->key() == Qt::Key_Escape) {  
       qApp->quit();
   } 
}

Bản thân các lớp có sẵn trong Qt đã có sẵn những slot tự động xử lý các signal riêng biệt. Tuy nhiên ta có thể code lại các slot này, trong lập trình hướng đối tượng thì khái niệm này được gọi là Overriding. Trong ví dụ này mình override lại slot keyPressEvent của lớp QWidget, slot này nhận thông tin của signal trong lớp QKeyEvent.

#include <QApplication>
#include "keypress.h"

int main(int argc, char *argv[]) {

  QApplication app(argc, argv);  
    
  KeyPress window;
  
  window.resize(250, 150);
  window.setWindowTitle("Key press");
  window.show();

  return app.exec();
}

QMoveEvent

Lớp QMoveEvent chứa các thông tin về sự thay đổi vị trí của widget. QMoveEvent sẽ tự động được gửi tới widget như keyPress ở trên.

#pragma once

#include <QMainWindow>

class Move : public QWidget {

  Q_OBJECT

  public:
    Move(QWidget *parent = 0);
 
  protected:
    void moveEvent(QMoveEvent *e);
};
#include <QMoveEvent>
#include "move.h"

Move::Move(QWidget *parent)
    : QWidget(parent)
{ }

void Move::moveEvent(QMoveEvent *e) {

  int x = e->pos().x();
  int y = e->pos().y();
  
  QString text = QString::number(x) + "," + QString::number(y);

  setWindowTitle(text);
}

Trong đoạn code trên, chúng ta xử lý sự kiện di chuyển widget. Cứ mỗi lần chúng ta di chuyển cửa sổ, một event được gửi tới slot moveEvent, cũng giống như keyPress, slot này có sẵn trong Qt và chúng ta cũng override lại nó. Chúng ta lấy vị trí tọa độ mới của widget và đưa lên thanh tiêu đề.

int x = e->pos().x();
int y = e->pos().y();

Lấy thông tin tọa độ từ đối tượng QMoveEvent.

QString text = QString::number(x) + "," + QString::number(y);

Chuyển giá trị tọa độ từ số sang chuỗi.

setWindowTitle(text);

Dùng phương thức setWindowTitle() để đặt tiêu đề cho cửa sổ.

#include <QApplication>
#include "move.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  Move window;
  
  window.resize(250, 150);
  window.setWindowTitle("Move");
  window.show();
  
  return app.exec();
}
Capture

Ngắt kết nối signal

Nếu có kết nối signal và slot thì Qt cũng cho phép chúng ta ngắt kết nối slot với các signal đó.

#pragma once

#include <QWidget>
#include <QPushButton>

class Disconnect : public QWidget {
    
  Q_OBJECT  

  public:
    Disconnect(QWidget *parent = 0);

  private slots:
    void onClick();
    void onCheck(int);

  private:
    QPushButton *clickBtn;
};

Ở trên chúng ta khai báo hai slot. Từ khóa slot thực chất không phải của C++, mà là phần mở rộng của C++ do Qt phát triển thêm. Chúng sẽ được dịch bởi Qt trước khi được dịch bởi C++. Để có thể dùng được signal và slot trong Qt thì chúng ta phải thêm macro Q_OBJECT vào đầu mỗi khai báo lớp.

#include <QTextStream>
#include <QCheckBox>
#include <QHBoxLayout>
#include "disconnect.h"

Disconnect::Disconnect(QWidget *parent)
    : QWidget(parent) {
        
  QHBoxLayout *hbox = new QHBoxLayout(this);
  hbox->setSpacing(5);        
        
  clickBtn = new QPushButton("Click", this);
  hbox->addWidget(clickBtn, 0, Qt::AlignLeft | Qt::AlignTop);

  QCheckBox *cb = new QCheckBox("Connect", this);
  cb->setCheckState(Qt::Checked);
  hbox->addWidget(cb, 0, Qt::AlignLeft | Qt::AlignTop);

  connect(clickBtn, &QPushButton::clicked, this, &Disconnect::onClick);
  connect(cb, &QCheckBox::stateChanged, this, &Disconnect::onCheck);  
}

void Disconnect::onClick() {
    
  QTextStream out(stdout);
  out << "Button clicked" << endl;
}

void Disconnect::onCheck(int state) {
    
  if (state == Qt::Checked) {
    connect(clickBtn, &QPushButton::clicked, this, &Disconnect::onClick);
  } else {
    disconnect(clickBtn, &QPushButton::clicked, this, 
        &Disconnect::onClick);
  }
}

Trong ví dụ này chúng ta tạo ra một button và một checkbox. Nếu tick vào checkbox thì cho connect button với slot onClick, ngược lại thì ngắt kết nối. Cứ mỗi lần bấm button thì in ra message, tuy nhiên message này hiển thị trên console vì thế nếu bạn nên chạy chương trình từ command promtp hoặc nhìn vào cửa sổ Application Output trên Qt Creator mới thấy message.

connect(clickBtn, &QPushButton::clicked, this, &Disconnect::onClick);
connect(cb, &QCheckBox::stateChanged, this, &Disconnect::onCheck); 

Connect hai signal với hai slot.

void Disconnect::onClick() {
    
  QTextStream out(stdout);
  out << "Button clicked" << endl;
}

Nếu nhấn button thì in message.

void Disconnect::onCheck(int state) {
    
  if (state == Qt::Checked) {
    connect(clickBtn, &QPushButton::clicked, this, &Disconnect::onClick);
  } else {
    disconnect(clickBtn, &QPushButton::clicked, this, &Disconnect::onClick);
  }
}

Slot onCheck() sẽ kết nối hoặc ngắt kết nối button với slot. Chúng ta dùng phương thức disconnect() để ngắt.

#include <QApplication>
#include "disconnect.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  Disconnect window;
  
  window.resize(250, 150);
  window.setWindowTitle("Disconnect");
  window.show();
  
  return app.exec();
}

Capture

Timer

Timer trong Qt (hay các nền tảng khác) thương được dùng để code các công việc có tính chất lặp đi lặp lại. Trong ví dụ này ta dùng timer để hiển thị giờ hiện tại.

#pragma once

#include <QWidget>
#include <QLabel>

class Timer : public QWidget {

  public:
    Timer(QWidget *parent = 0);

  protected:
    void timerEvent(QTimerEvent *e);

  private:
    QLabel *label;
};
#include "timer.h"
#include <QHBoxLayout>
#include <QTime>

Timer::Timer(QWidget *parent)
    : QWidget(parent) {
        
  QHBoxLayout *hbox = new QHBoxLayout(this);
  hbox->setSpacing(5);               
           
  label = new QLabel("", this);
  hbox->addWidget(label, 0, Qt::AlignLeft | Qt::AlignTop);

  QTime qtime = QTime::currentTime();
  QString stime = qtime.toString();
  label->setText(stime);
   
  startTimer(1000);
}

void Timer::timerEvent(QTimerEvent *e) {
    
  Q_UNUSED(e);
  
  QTime qtime = QTime::currentTime();
  QString stime = qtime.toString();
  label->setText(stime);
}

Chúng ta hiển thị giờ từ hệ thống.

QTime qtime = QTime::currentTime();
QString stime = qtime.toString();
label->setText(stime);

Chúng ta lấy giờ hiện tại của hệ thống bằng phương thức currentTime(), sau đó chuyển nó sang kiểu chuỗi và đưa vào label.

startTimer(1000);

Phương thức startTimer() sẽ bắt đầu chu kỳ lặp của nó, cứ sau 1000ms nó sẽ phát một signal và signal đó sẽ được xử lý bởi slot timerEvent().

void Timer::timerEvent(QTimerEvent *e) {
    
  Q_UNUSED(e);
  
  QTime qtime = QTime::currentTime();
  QString stime = qtime.toString();
  label->setText(stime);
}

Để bắt được event từ timer, chúng ta phải override phương thức timerEvent().

#include <QApplication>
#include "timer.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);  
    
  Timer window;
  
  window.resize(250, 150);
  window.setWindowTitle("Timer");
  window.show();

  return app.exec();
}

Capture