Author Archives: Phở Code

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

Qt 5 C++ – Layout

Trong phần này các bạn sẽ được học cách làm việc với các loại layout có trong Qt 5, mình sẽ đề cập đến bốn loại layout là QHBoxLayout, QVBoxLayout, QFormLayout, và QGridLayout.

Nói về layout thì khi bạn viết một ứng dụng, bạn sẽ phải dùng đến các widget (hay control trong Visual Studio) như nút bấm, label… trong Qt 5 bạn có hai cách để thiết kế chúng, một là tự quy định vị trí, kích thước, cái này gọi là Absolute Positioning, hai là dùng các layout, các layout sẽ quản lý widget cho bạn.

Absolute Positioning

Như đã nói ở trên, bạn sẽ tự định vị vị trí cũng như kích thước của từng widget theo tọa độ pixel. Và đây là những vấn đề bạn sẽ gặp nếu thiết kế theo kiểu Absolute Positioning:

  • Kích thước của các widget sẽ không tự động thay đổi khi cửa sổ chính thay đổi.
  • Ứng dụng nhìn sẽ rất khác (thường là xấu hơn) trên từng hệ điều hành.
  • Thay đổi kiểu font cũng sẽ làm hỏng giao diện.
  • Nếu bạn muốn thay đổi giao diện, chẳng hạn thêm/bớt nút bấm, di chuyển textbox lên xuống… bạn sẽ phải làm lại từ đầu, như thế rất tốn thời gian và không chuyên nghiệp.

Tuy sẽ có những trường hợp mà bạn phải làm theo kiểu absolute. Còn lại thì hầu hết đều nên dùng đến layout.

#include <QApplication>
#include <QDesktopWidget>
#include <QTextEdit>

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

Absolute::Absolute(QWidget *parent)
    : QWidget(parent) {
        
  QTextEdit *ledit = new QTextEdit(this);
  ledit->setGeometry(5, 5, 200, 150);
}

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

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

  return app.exec();
}

Trong ví dụ trên chúng ta tạo ra widget QTextEdit và thiết kế nó bằng tay. Phương thức setGeometry() quy định vị trí và kích thước của widget.

Capture
Trước khi thay đổi kích thước window
Capture
Sau khi thay đổi kích thước window

QVBoxLayout

Lớp QVBoxLayout sẽ sắp xếp các widget theo hàng dọc. Để thêm widget vào layout thì ta dùng phương thức addWidget().

#pragma once

#include <QWidget>

class VerticalBox : public QWidget {

  public:
    VerticalBox(QWidget *parent = 0);
};
#include "verticalbox.h"
#include <QVBoxLayout>
#include <QPushButton>

VerticalBox::VerticalBox(QWidget *parent)
    : QWidget(parent) {

  QVBoxLayout *vbox = new QVBoxLayout(this);
  vbox->setSpacing(1);
  
  QPushButton *settings = new QPushButton("Settings", this);
  settings->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
  QPushButton *accounts = new QPushButton("Accounts", this);
  accounts->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
  QPushButton *loans = new QPushButton("Loans", this);
  loans->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
  QPushButton *cash = new QPushButton("Cash", this);
  cash->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
  QPushButton *debts = new QPushButton("Debts", this);
  debts->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);

  vbox->addWidget(settings);
  vbox->addWidget(accounts);
  vbox->addWidget(loans);
  vbox->addWidget(cash);
  vbox->addWidget(debts);

  setLayout(vbox);
}

Trong ví dụ trên, chúng ta có một layout và năm button. Mỗi button sẽ dãn ra theo cả hai chiều ngang và dọc.

QVBoxLayout *vbox = new QVBoxLayout(this);
vbox->setSpacing(1);

Tạo một đối tượng QVBoxLayout, phương thức setSpacing() quy định khoảng cách giữa các widget, trong trường hợp này là 1 pixel.

QPushButton *settings = new QPushButton("Settings", this);
settings->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);

Với mỗi button được tạo ra, chúng ta quy định kích thước cho chúng bằng phương thức setSizePolicy(), tham số đầu tiên là kích thước chiều ngang, tham số thứ hai là kích thước chiều dọc. Mặc định trong Qt thì chiều ngang sẽ tự động dãn nở, còn chiều rộng thì không. Hằng số QSizePolicy::Expanding quy định kiểu co dãn là tự động co dãn.

vbox->addWidget(settings);
vbox->addWidget(accounts);
...

Sử dụng phương thức addWidget() để đưa widget vào layout.

setLayout(vbox);

Cuối cùng set layout cho window.

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

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

  window.resize(240, 230);
  window.setWindowTitle("VerticalBox");
  window.show();

  return app.exec();
}
Capture

Bạn có thể thử kéo dãn cửa sổ window để thấy các button cũng tự co dãn theo.

Buttons

Trong ví dụ dưới đây, chúng ta sẽ hiển thị hai button nằm ở góc dưới bên phải cửa sổ.

#pragma once

#include <QWidget>
#include <QPushButton>

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

  private:
    QPushButton *okBtn;
    QPushButton *applyBtn;
};
#include "buttons.h"
#include <QVBoxLayout>
#include <QHBoxLayout>

Buttons::Buttons(QWidget *parent)
    : QWidget(parent) {

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

  okBtn = new QPushButton("OK", this);
  applyBtn = new QPushButton("Apply", this);

  hbox->addWidget(okBtn, 1, Qt::AlignRight);
  hbox->addWidget(applyBtn, 0);

  vbox->addStretch(1);
  vbox->addLayout(hbox);
}
QVBoxLayout *vbox = new QVBoxLayout(this);
QHBoxLayout *hbox = new QHBoxLayout();

Chúng ta tạo hai layout, QVBoxLayoutQHBoxLayout, trong đó QHBoxLayout cũng tương tự như QVBoxLayout nhưng sắp xếp các widget theo chiều ngang.

hbox->addWidget(okBtn, 1, Qt::AlignRight);
hbox->addWidget(applyBtn, 0);

Các button được đặt trong layout nằm ngang, và được canh lề về phía bên phải với phương thức addWidget().Ở đây chúng ta dùng ba tham số cho phương thức này (khác với các phần trước chỉ có một), tham số đầu tiên là biến widget được thêm vào, tham số thứ hai là chỉ số stretch (co dãn), tham số thứ ba là chỉ số align (canh lề).

Về tham số stretch, tham số stretch nhận giá trị là các số nguyên, quy định widget sẽ chiếm bao nhiêu phần trong tổng số chiều ngang (hoặc dọc) của layout. Mặc định tham số này là 0, khi đó chiều ngang (hoặc dọc) sẽ được chia đều cho các widget. Khi có một widget có chỉ số 1, các widget còn lại mang chỉ số 0, thì các widget 0 sẽ nhận chiều ngang vừa đủ bao bọc lấy tiêu đề của nó, còn widget 1 sẽ chiếm toàn bộ chiều ngang còn lại. Khi các widget này có chỉ số khác 0 với 1, thì chúng sẽ được chia đều theo đúng tỉ lệ đó. Ví dụ nút OK có stretch là 3, nút Apply có stretch là 4 chẳng hạn, thì layout sẽ chia chiều ngang theo đúng tỉ lệ 3:4 cho hai nút này.

Về tham số align, tham số này nhận bốn giá trị là Qt::AlignCenter, Qt::AlignRight, Qt::AlignLèt và no alignment (cái này là mặc định nếu bạn ko đưa giá trị nào vào), đối với no alignment thì widget sẽ tự động dãn chiều ngang/dọc của nó cho đủ tỉ lệ đã được layout phân chia. Còn lại ba loại kia thì widget sẽ tự thu nhỏ chiều ngang/dọc của nó cho vừa đủ với tiêu đề của nó rồi tự nằm đúng vị trí bên trái, phải hoặc giữa với khu vực đã được layout phân chia.

Nếu đoạn giải thích trên có vẻ hơi khó hiểu thì bạn có thể tự điều chỉnh các tham số để hiểu thêm về cách hoạt động của nó hoặc comment hỏi ở dưới bài, mình sẽ giải thích rõ hơn.

vbox->addStretch(1);
vbox->addLayout(hbox);

Ở đây mình sử dụng phương thức addStretch(), phương thức này sẽ đưa một khoảng trống vào layout, đối với QVBoxLayout (layout dọc) thì khoảng trống này sẽ được đưa từ trên xuống, với layout ngang là từ trái qua phải, khoảng trống được đưa vào sẽ lấp đầy phần khoảng trống dư thừa do các widget không đủ kích thước để lấp.

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

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

  Buttons window;

  window.resize(290, 170);
  window.setWindowTitle("Buttons");
  window.show();
  
  return app.exec();
}
Capture

Kết hợp các layout

Đoạn code ví dụ dưới đây kết hợp các layout lại với nhau thay vì chỉ dùng một-hai layout. Việc kết hợp các layout lại sẽ làm cho giao diện trở nên tinh xảo hơn. Để kết hợp layout thì chúng ta dùng phương thức addLayout().

#pragma once

#include <QWidget>

class Layouts : public QWidget {

  public:
    Layouts(QWidget *parent = 0);
};
#include <QVBoxLayout>
#include <QPushButton>
#include <QListWidget>
#include "nesting.h"

Layouts::Layouts(QWidget *parent)
    : QWidget(parent) {

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

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

  QPushButton *add = new QPushButton("Add", this);
  QPushButton *rename = new QPushButton("Rename", this);
  QPushButton *remove = new QPushButton("Remove", this);
  QPushButton *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);

  setLayout(hbox);
}

Chúng ta tạo ra một window có chứa bốn button và một list widget. Các button nằm trong vertical layout (layout chiều dọc), bản thân layout này được đặt trong một horizontal layout (layout ngang) và được đặt bên phải layout. Ngoài ra bên trái layout ngang này còn chứa list widget. Nếu chúng ta co dãn kích thước window thì kích thước của list widget cũng tự động co dãn theo.

QVBoxLayout *vbox = new QVBoxLayout();

QVBoxLayout để chứa các button.

QHBoxLayout *hbox = new QHBoxLayout(this);

QHBoxLayout là layout chính cho toàn bộ widget.

QListWidget *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à thêm các item cho nó.

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

Tạo bốn button.

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

Tạo vertical layout, thêm các tùy chỉnh như khoảng cách giữa các widget và chỉ số stretch cho nó.

hbox->addWidget(lw);
hbox->addSpacing(15);
hbox->addLayout(vbox);

Đặt list widget vào trong horizontal layout. Sau đó đưa vertical layout vào horizontal layout bằng phương thức addLayout().

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

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

  return app.exec();
}
Capture

QFormLayout

QFormLayout là layout trợ giúp chúng ta trong việc tạo các form (biểu mẫu). Layout này chia thiết kế làm hai cột, một bên là tên các trường, một bên là các trường để điền, có thể là các text box hay spin box. Xem ví dụ.

#pragma once

#include <QWidget>

class FormEx : public QWidget {
    
  public:
    FormEx(QWidget *parent = 0);
};
#include <QFormLayout>
#include <QLabel>
#include <QLineEdit>
#include "form.h"

FormEx::FormEx(QWidget *parent)
    : QWidget(parent) {
        
  QLineEdit *nameEdit = new QLineEdit(this);
  QLineEdit *addrEdit = new QLineEdit(this);
  QLineEdit *occpEdit = new QLineEdit(this);
  
  QFormLayout *formLayout = new QFormLayout;
  formLayout->setLabelAlignment(Qt::AlignRight | Qt::AlignVCenter);
  formLayout->addRow("Name:", nameEdit);
  formLayout->addRow("Email:", addrEdit);
  formLayout->addRow("Age:", occpEdit);
  
  setLayout(formLayout);
}

Đoạn code trên tạo ra ba label và ba line edit (hay textbox trong visual studio).

QFormLayout *formLayout = new QFormLayout;

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

formLayout->setLabelAlignment(Qt::AlignRight | Qt::AlignVCenter);

Căn chỉnh lề cho cột label bằng phương thức setLabelAlignment(), các bạn có thể dùng các tham số khác nhau để thử nghiệm.

formLayout->addRow("Name:", nameEdit);

Phương thức  addRow() sẽ thêm một dòng vào trong form, tham số đầu tiên là label sẽ được hiển thị, tham số thứ hai là biến widget.

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

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

  FormEx window;

  window.setWindowTitle("Form example");
  window.show();

  return app.exec();
}

.Capture

QGridLayout

Lớp QGridLayout thiết kế các widget theo dạng lưới (hay bảng).

#pragma once

#include <QWidget>

class Calculator : public QWidget {

  public:
    Calculator(QWidget *parent = 0);

};
#include <QGridLayout>
#include <QPushButton>
#include "calculator.h"

Calculator::Calculator(QWidget *parent)
    : QWidget(parent) {

  QGridLayout *grid = new QGridLayout(this);
  grid->setSpacing(2);

  QList<QString> values({ "7", "8", "9", "/", 
    "4", "5", "6", "*",
    "1", "2", "3", "-",
    "0", ".", "=", "+"
  });
  
  int pos = 0;
  
  for (int i=0; i<4; i++) {
   for (int j=0; j<4; j++) { QPushButton *btn = new QPushButton(values[pos], this); btn->setFixedSize(40, 40);
     grid->addWidget(btn, i, j);
     pos++;
   }
  }  
  
  setLayout(grid);
}

Chúng ta sẽ thiết kế giao diện của một chương trình máy tính bỏ túi.

QGridLayout *grid = new QGridLayout(this);
grid->setSpacing(2);

Tạo đối tượng QGridLayout và quy định khoảng cách giữa các widget là 2 pixel.

QList<QString> values({ "7", "8", "9", "/", 
  "4", "5", "6", "*",
  "1", "2", "3", "-",
  "0", ".", "=", "+"
});

Tạo danh sách các tên các nút bấm cần đưa vào.

for (int i=0; i<4; i++) {
  for (int j=0; j<4; j++) { QPushButton *btn = new QPushButton(values[pos], this); btn->setFixedSize(40, 40);
      grid->addWidget(btn, i, j);
      pos++;
  }
} 

Cuối cùng đưa 16 widget nút bấm vô layout. Các nút bấm sẽ có kích thước cố định như nhau.

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

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

  QApplication app(argc, argv); 

  Calculator window;

  window.move(300, 300);
  window.setWindowTitle("Calculator");
  window.show();

  return app.exec();
}
Capture

Tổng kết

Để kết thúc bài chúng ta sẽ tạo ra một giao diện phức tạp một tí bằng QGridLayout :).

#pragma once

#include <QWidget>

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

Review::Review(QWidget *parent)
    : QWidget(parent) {

  QGridLayout *grid = new QGridLayout(this);
  grid->setVerticalSpacing(15);
  grid->setHorizontalSpacing(10);

  QLabel *title = new QLabel("Title:", this);
  grid->addWidget(title, 0, 0, 1, 1);
  title->setAlignment(Qt::AlignRight | Qt::AlignVCenter);

  QLineEdit *edt1 = new QLineEdit(this);
  grid->addWidget(edt1, 0, 1, 1, 1);

  QLabel *author = new QLabel("Author:", this);
  grid->addWidget(author, 1, 0, 1, 1);
  author->setAlignment(Qt::AlignRight | Qt::AlignVCenter);

  QLineEdit *edt2 = new QLineEdit(this);
  grid->addWidget(edt2, 1, 1, 1, 1);

  QLabel *review = new QLabel("Review:", this);
  grid->addWidget(review, 2, 0, 1, 1);
  review->setAlignment(Qt::AlignRight | Qt::AlignTop);

  QTextEdit *te = new QTextEdit(this);
  grid->addWidget(te, 2, 1, 3, 1);

  setLayout(grid);
}

Trong ví dụ trên, ta tạo ra một form thường dùng để đánh giá sách, các trường gồm có tên tác giả, tiêu đề, nội dung đánh giá.

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

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

  Review window;

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

  return app.exec();
}
Capture

Qt 5 C++ – Menu và toolbar

Trong bài này chúng ta sẽ học về cách làm menu và toolbar trong một ứng dụng Qt 5.

Ví dụ

Đoạn code dưới đây sẽ hiển thị một menu đơn giản.

#pragma once

#include <QMainWindow>
#include <QApplication>

class SimpleMenu : public QMainWindow {

  public:
    SimpleMenu(QWidget *parent = 0);
};
#include "simplemenu.h"
#include <QMenu>
#include <QMenuBar>

SimpleMenu::SimpleMenu(QWidget *parent)
    : QMainWindow(parent) {
    
  QAction *quit = new QAction("&Quit", this);

  QMenu *file;
  file = menuBar()->addMenu("&File");
  file->addAction(quit);

  connect(quit, &QAction::triggered, qApp, QApplication::quit);
}

Đoạn code trên sẽ tạo ra menu File. Một lớp muốn có menu thì lớp này phải kế thừa từ QMainWindow.

QAction *quit = new QAction("&Quit", this);

Dòng code trên tạo một đối tượng QAction. Một action là một lớp chuyên để thực hiện một lệnh nào đó. Lý do tại sao nên dùng action thì bạn để ý trong một ứng dụng GUI thông thường có rất nhiều cách để thi hành một công việc nào đó, chẳng hạn muốn thoát Qt Creator thì bạn có thể bấm vào dấu “X” trên góc phải của Qt Creator, hoặc vào menu File→Quit, hoặc bấm Ctrl+Q, tất cả ba cách trên đều làm chung một việc, nên tốt nhất là ta connect cả ba cách đó vào một action. Một QMenu có thể có một hoặc nhiều action.

QMenu *file;
file = menuBar()->addMenu("&File");

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

file->addAction(quit);

Dùng phương thức addAction() để thêm action.

connect(quit, &QAction::triggered, qApp, QApplication::quit);

Sau đó connect action này với phương thức quit() của qApp. Nếu chưa biết qApp là gì thì bạn có thể tìm phần trước của bài này để đọc.

#include "simplemenu.h"

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

  QApplication app(argc, argv);  
    
  SimpleMenu window;

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

  return app.exec();
}

Untitled

Icon, shortcut, separator

Trong phần này chúng ta sẽ tùy biến menu bằng cách thêm icon, phím tắt (shortcut) và các dòng phân cách – separator.

#pragma once

#include <QMainWindow>
#include <QApplication>

class AnotherMenu : public QMainWindow {
    
  public:
    AnotherMenu(QWidget *parent = 0);
};
#include "anothermenu.h"
#include <QMenu>
#include <QMenuBar>

AnotherMenu::AnotherMenu(QWidget *parent)
    : QMainWindow(parent) {
          
  QPixmap newpix("new.png");
  QPixmap openpix("open.png");
  QPixmap quitpix("quit.png");

  QAction *newa = new QAction(newpix, "&New", this);
  QAction *open = new QAction(openpix, "&Open", this);
  QAction *quit = new QAction(quitpix, "&Quit", this);
  quit->setShortcut(tr("CTRL+Q"));

  QMenu *file;
  file = menuBar()->addMenu("&File");
  file->addAction(newa);
  file->addAction(open);
  file->addSeparator();
  file->addAction(quit);
  
  qApp->setAttribute(Qt::AA_DontShowIconsInMenus, false);

  connect(quit, &QAction::triggered, qApp, &QApplication::quit);
}

Trong ví dụ này chúng ta tạo ra một menu với ba action, nhưng chỉ dùng tới action quit thôi. Ngoài ra ta sẽ thêm một dòng phân cách và phím tắt Ctrl+Q. 

QPixmap newpix("new.png");
QPixmap openpix("open.png");
QPixmap quitpix("quit.png");

Đây là hình icon cho menu. Lưu ý có một số môi trường không cho phép hiển thị icon trên menu.

QAction *newa = new QAction(newpix, "&New", this);
QAction *open = new QAction(openpix, "&Open", this);
QAction *quit = new QAction(quitpix, "&Quit", this);

Đoạn code trên tạo QAction với tham số đầu tiên là icon mà nó sẽ dùng.

quit->setShortcut(tr("CTRL+Q"));

Tạo phím tắt, cứ mỗi lần bấm tổ hợp đúng tổ hợp phím thì chương trình sẽ thoát.

file->addSeparator();

Thêm dòng phân cách. Dòng phân cách chẳng qua chỉ là một đường kẻ ngang để gộp nhóm một số action có chung đặc điểm lại với nhau.

qApp->setAttribute(Qt::AA_DontShowIconsInMenus, false);

Như đã nói ở trên, trong một số môi trường icon sẽ không được hiển thị, nhưng chúng ta có thể bắt nó hiển thị bằng cách đưa thuộc tính Qt::AA_DontShowIconsInMenus thành false.

#include "anothermenu.h"

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

  QApplication app(argc, argv);  
    
  AnotherMenu window;

  window.resize(350, 200);
  window.setWindowTitle("Simple menu");
  window.show();

  return app.exec();
}

File main.

Untitled

Check menu

Ví dụ dưới đây sẽ tạo ra menu có thể check được. Chúng ta sẽ tạo ra một thanh trạng thái (statusbar), cứ mỗi lần check hoặc bỏ check menu này thì statusbar sẽ hiện hoặc ẩn.

#pragma once

#include <QMainWindow>
#include <QApplication>

class Checkable : public QMainWindow {
    
  Q_OBJECT  

  public:
    Checkable(QWidget *parent = 0);
 
  private slots:
    void toggleStatusbar();

  private:
    QAction *viewst;
};
#include "checkable.h"
#include <QMenu>
#include <QMenuBar>
#include <QStatusBar>

Checkable::Checkable(QWidget *parent)
    : QMainWindow(parent) {

  viewst = new QAction("&View statusbar", this);
  viewst->setCheckable(true);
  viewst->setChecked(true);

  QMenu *file;
  file = menuBar()->addMenu("&File");
  file->addAction(viewst);

  statusBar();

  connect(viewst, &QAction::triggered, this, &Checkable::toggleStatusbar);
}

void Checkable::toggleStatusbar() {
    
  if (viewst->isChecked()) {
      
      statusBar()->show();
  } else {
      
      statusBar()->hide();
  }
}

Check menu sẽ làm ẩn hoặc hiện statusbar.

viewst = new QAction("&View statusbar", this);
viewst->setCheckable(true);
viewst->setChecked(true);

Phương thức setCheckable() để chuyển menu thành dạng có thể check được. Rồi dùng phương thức setChecked() để làm cho menu đó đã check rồi nếu thích.

if (viewst->isChecked())     
    statusBar()->show();
else     
    statusBar()->hide();

Phương thức toggleStatusbar() sẽ làm cho thanh statusbar ẩn hoặc hiện.

#include "checkable.h"

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

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

  return app.exec();
}

Untitled

QToolBar

Lớp QToolBar sẽ hiển thị một thanh ngang chứa các nút lệnh cho ứng dụng, ngoài ra bạn có thể dùng chuột kéo thanh này sang trái, phải hoặc nằm dưới.

#pragma once

#include <QMainWindow>
#include <QApplication>

class Toolbar : public QMainWindow {
    
  Q_OBJECT  

  public:
    Toolbar(QWidget *parent = 0);
};
#include "toolbar.h"
#include <QToolBar>
#include <QIcon>
#include <QAction>

Toolbar::Toolbar(QWidget *parent)
    : QMainWindow(parent) {
    
  QPixmap newpix("new.png");
  QPixmap openpix("open.png");
  QPixmap quitpix("quit.png");
  
  QToolBar *toolbar = addToolBar("main toolbar");
  toolbar->addAction(QIcon(newpix), "New File");
  toolbar->addAction(QIcon(openpix), "Open File");
  toolbar->addSeparator();
  QAction *quit = toolbar->addAction(QIcon(quitpix), 
      "Quit Application");
  
  connect(quit, &QAction::triggered, qApp, &QApplication::quit);
}

Lưu ý là luôn phải kế thừa từ lớp QMainWindow nếu muốn tạo menu hoặc toolbar.

QToolBar *toolbar = addToolBar("main toolbar");

Phương thức addToolBar() sẽ tạo một toolbar.

toolbar->addAction(QIcon(newpix), "New File");
toolbar->addAction(QIcon(openpix), "Open File");
toolbar->addSeparator();

Tạo hai action và một separator.

#include "toolbar.h"

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

  window.resize(300, 200);
  window.setWindowTitle("QToolBar");
  window.show();

  return app.exec();
}

Capture

Khung chương trình mẫu

Để kết thúc bài này, chúng ta sẽ tạo ra một khung chương trình mẫu.

#pragma once

#include <QMainWindow>
#include <QApplication>

class Skeleton : public QMainWindow {
    
  Q_OBJECT  

  public:
    Skeleton(QWidget *parent = 0);
};
#include <QMenu>
#include <QMenuBar>
#include <QStatusBar>
#include <QTextEdit>
#include <QToolBar>
Skeleton::Skeleton(QWidget *parent)
    : QMainWindow(parent) {
        
  QPixmap newpix("new.png");
  QPixmap openpix("open.png");
  QPixmap quitpix("quit.png");

  QAction *quit = new QAction("&Quit", this);

  QMenu *file;
  file = menuBar()->addMenu("&File");
  file->addAction(quit);

  connect(quit, &QAction::triggered, qApp, &QApplication::quit);
  
  QToolBar *toolbar = addToolBar("main toolbar");
  toolbar->addAction(QIcon(newpix), "New File");
  toolbar->addAction(QIcon(openpix), "Open File");
  toolbar->addSeparator();
  
  QAction *quit2 = toolbar->addAction(QIcon(quitpix), 
      "Quit Application");
  connect(quit2, &QAction::triggered, qApp, &QApplication::quit);

  QTextEdit *edit = new QTextEdit(this);  

  setCentralWidget(edit);

  statusBar()->showMessage("Ready");
}

Trong ví dụ này  chúng ta tạo cả toolbar và statusbar.

QTextEdit *edit = new QTextEdit(this);  

setCentralWidget(edit);

Tạo một đối tượng QTextEdit và đặt nó ngay giữa QMainWindow.

#include "skeleton.h"

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

  window.resize(350, 250);
  window.setWindowTitle("Application skeleton");
  window.show();

  return app.exec();
}

Capture

Qt 5 C++ – GUI

Những bài trước chủ yếu ta làm việc qua console. Từ bài này chúng ta sẽ bắt đầu làm việc với GUI, widgets.

Ở đây chúng ta sẽ cho hiển thị icon, tooltip. Hiển thị cửa sổ window ở giữa màn hình. Sau đó tìm hiểu qua về cơ chế Signal và Slot.

Ví dụ

#include <QApplication>
#include <QWidget>

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

    QApplication app(argc, argv);

    QWidget window;

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

    return app.exec();
}

Ví dụ trên sẽ hiển thị một cửa sổ lên màn hình.

QApplication app(argc, argv);

Đây là đối tượng QApplication. Bất cứ project nào trong Qt 5 đều phải có dòng này. Trừ project console thì dùng QCoreApplication.

QWidget window;

Bạn có thể hiểu lớp QWidget là một lớp window, chuyên để tạo window :).

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

Thay đổi kích thước của widget, thêm tiêu đề cho widget rồi hiển thị lên màn hình.

return app.exec();

Cuối cùng app.exec() sẽ chạy chương trình.

Capture

Tooltip

Tooltip chính là các câu gợi ý khi bạn di chuyển chuột vào vị trí nào đó. Xem ví dụ.

#include <QApplication>
#include <QWidget>

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

  QApplication app(argc, argv);  

  QWidget window;

  window.resize(250, 150);
  window.move(300, 300);
  window.setWindowTitle("Simple Example");
  window.setToolTip("This is a tooltip");
  window.show();

  return app.exec();
}

Ví dụ này sẽ show tooltip khi bạn di chuyển chuột trên widget.

window.setWindowTitle("ToolTip");

Phương thức setToolTip() sẽ làm việc này.

Untitled

Icon

Chắc bạn cũng chẳng còn lạ lẫm gì với Icon. Xem ví dụ.

#include <QApplication>
#include <QWidget>
#include <QIcon>

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

  QApplication app(argc, argv);  
    
  QWidget window;

  window.resize(250, 150);
  window.setWindowTitle("Icon");
  window.setWindowIcon(QIcon("web.png"));
  window.show();

  return app.exec();
}

Icon sẽ hiện lên trên góc trái của widget.

window.setWindowIcon(QIcon("web.png"));

Để hiện icon thì bạn dùng phương thức setWindowIcon() và đưa vào đường dẫn đến một file ảnh bất kỳ.

Capture

Con trỏ chuột

#include <QApplication>
#include <QWidget>
#include <QFrame>
#include <QGridLayout>

class Cursors : public QWidget {

 public:
     Cursors(QWidget *parent = 0);
};

Cursors::Cursors(QWidget *parent)
    : QWidget(parent) {
    
  QFrame *frame1 = new QFrame(this);
  frame1->setFrameStyle(QFrame::Box);
  frame1->setCursor(Qt::SizeAllCursor);

  QFrame *frame2 = new QFrame(this);
  frame2->setFrameStyle(QFrame::Box);
  frame2->setCursor(Qt::WaitCursor);

  QFrame *frame3 = new QFrame(this);
  frame3->setFrameStyle(QFrame::Box);
  frame3->setCursor(Qt::PointingHandCursor);

  QGridLayout *grid = new QGridLayout(this);
  grid->addWidget(frame1, 0, 0);
  grid->addWidget(frame2, 0, 1);
  grid->addWidget(frame3, 0, 2);

  setLayout(grid);
}

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

  QApplication app(argc, argv);  
    
  Cursors window;

  window.resize(350, 150);
  window.setWindowTitle("Cursors");
  window.show();

  return app.exec();
}

Code ví dụ trên sẽ hiển thị 3 hình vuông trên 1 cửa sổ, mỗi lần bạn di chuột vào hình vuông khác thì con trỏ chuột sẽ hiển thị kiểu khác.

QFrame *frame1 = new QFrame(this);

Tạo một đối tượng QFrame, mình sẽ nói về QFrame ở bài viết khác.

frame1->setFrameStyle(QFrame::Box);

Phương thức setFrameStyle() để set style cho nó là Box, như thế sẽ thấy được đường viền của nó.

frame1->setCursor(Qt::SizeAllCursor);

Phương thức setCursor() để set kiểu con trỏ chuột, dùng các kiểu có sẵn trong Qt.

QGridLayout *grid = new QGridLayout(this);
grid->addWidget(frame1, 0, 0);
grid->addWidget(frame2, 0, 1);
grid->addWidget(frame3, 0, 2);
setLayout(grid);

QGridLayout sẽ tự động điều chỉnh vị trí các frame theo kiểu bảng. Chúng ta sẽ học về các layout này trong các bài viết sau.

QPushButton

QPushButton là lớp widget dùng để hiển thị nút bấm. Ví dụ dưới đây sẽ hiển thị một button, khi click vào nút thì thoát chương trình.

#include <QApplication>
#include <QWidget>
#include <QPushButton>

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

MyButton::MyButton(QWidget *parent)
    : QWidget(parent) {
           
  QPushButton *quitBtn = new QPushButton("Quit", this);
  quitBtn->setGeometry(50, 40, 75, 30);

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

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

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

  return app.exec();
}

Ví dụ trên có sử dụng cơ chế Signal Slot.

QPushButton *quitBtn = new QPushButton("Quit", this);
quitBtn->setGeometry(50, 40, 75, 30);

Tạo đối tượng QPushButton. Điều chỉnh kích thước và đưa nó lên cửa sổ bằng phương thức setGeometry().

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

Khi click vào nút bấm, một signal sẽ được phát ra. Slot là một phương thức của một đối tượng nào đó được dùng để nhận signal và xử lý signal đó. Trong trường hợp này là đối tượng qApp, đây là một biến toàn cục tham chiếu đến chương trình của chúng ta, bạn muốn tìm hiểu về nó có thể tìm đọc thêm về QAplication.

Capture

Signal và Slot

Chúng ta sẽ tìm hiểu thêm về Signal và slot bằng cách viết một chương trình tăng-giảm số đếm, signal và slot là cách mà các đối tượng giao tiếp với nhau trong Qt.

#pragma once

#include <QWidget>
#include <QApplication>
#include <QPushButton>
#include <QLabel>

class PlusMinus : public QWidget {
    
  Q_OBJECT

  public:
    PlusMinus(QWidget *parent = 0);

  private slots:
    void OnPlus();
    void OnMinus();

  private:
    QLabel *lbl;

};
class PlusMinus : public QWidget {
    
  Q_OBJECT
...  

Bạn phải thêm Q_OBJECT vào đầu định nghĩa lớp nếu muốn sử dụng signal và slot.

#include "plusminus.h"
#include <QGridLayout>

PlusMinus::PlusMinus(QWidget *parent)
    : QWidget(parent) {
        
  QPushButton *plsBtn = new QPushButton("+", this);
  QPushButton *minBtn = new QPushButton("-", this);
  lbl = new QLabel("0", this);
  
  QGridLayout *grid = new QGridLayout(this);
  grid->addWidget(plsBtn, 0, 0);
  grid->addWidget(minBtn, 0, 1);
  grid->addWidget(lbl, 1, 1);

  setLayout(grid);  

  connect(plsBtn, &QPushButton::clicked, this, &PlusMinus::OnPlus);
  connect(minBtn, &QPushButton::clicked, this, &PlusMinus::OnMinus);
}

void PlusMinus::OnPlus() {
    
  int val = lbl->text().toInt();
  val++;
  lbl->setText(QString::number(val));
}

void PlusMinus::OnMinus() {
    
  int val = lbl->text().toInt();
  val--;
  lbl->setText(QString::number(val));
}

Chúng ta có hai button và một label. Hai button này sẽ tăng hoặc giảm biến đếm và hiển thị biến đếm lên label.

connect(plsBtn, &QPushButton::clicked, this, &PlusMinus::OnPlus);
connect(minBtn, &QPushButton::clicked, this, &PlusMinus::OnMinus);

Ở đây chúng ta định nghĩa từng slot cho signal clicked.

void PlusMinus::OnPlus() {
    
  int val = lbl->text().toInt();
  val++;
  lbl->setText(QString::number(val));
}

Trong phương thức OnPlus() chúng ta lấy biến đếm từ label. Label này chỉ trả về 1 string nên chúng ta phải chuyển sang kiểu số. Sau đó chúng ta tăng con số đó lên rồi chuyển ngược lại string để hiển thị lên label.

#include "plusminus.h"

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

  window.resize(300, 190);
  window.setWindowTitle("Plus minus");
  window.show();

  return app.exec();
}

File main.cpp.
Capture

Qt 5 C++ – File và thư mục

Trong phần này chúng ta sẽ học về cách làm việc với file và thư mục.

QFile, QDir, và QFileInfo là các lớp chuyên làm việc với file trong Qt5. QFile cho phép bạn đọc và ghi file, QDir cho phép bạn truy cập và xem nội dung thư mục. QFileInfo cung cấp các thông tin về file độc lập với hệ thống, bao gồm tên file, đường dẫn thư mục, thời gian chỉnh sửa lần cuối, quyền đọc ghi hay quyền sở hữu.

Kích thước file

Ví dụ dưới đây lấy kích thước của một file

#include <QTextStream>
#include <QFileInfo>

int main(int argc, char *argv[]) {
    
  QTextStream out(stdout);

  if (argc != 2) {
      
    qWarning("Usage: file_size file");
    return 1;
  }
  
  QString filename = argv[1];
  
  if (!QFile(filename).exists()) {
  
    qWarning("The file does not exist");
    return 1;
  }  
  
  QFileInfo fileinfo(filename);
  
  qint64 size = fileinfo.size();
  
  QString str = "The size is: %1 bytes";
  
  out << str.arg(size) << endl;
}

Kích thước file có thể truy xuất thông qua phương thức size().

QString filename = argv[1];

Chúng ta lấy tên file qua tham số truyền vào chương trình.

if (!QFile(filename).exists()) {
  qWarning("The file does not exist");
  return 1;
}

Kiểm tra xem file đã tồn tại hay chưa bằng phương thức exists(), nếu chưa tồn tại thì cho in thông báo và thoát chương trình.

QString str = "The size is: %1 bytes";

out << str.arg(size) << endl;

In kết quả ra màn hình.

The size is: 28029 bytes

Đọc file trong Qt

Muốn đọc file thì trước tiên phải mở file đó ra đã. Các phương thức mở file sẽ trả về một stream để đọc file, nếu đã từng lập trình với Java hay C# thì có lẽ bạn đã từng được học về khái niệm stream, còn nếu chưa thì bạn cứ hiểu stream giống như std::cin hay std::cout trong thư viện <iostream> của C++ ấy, chỉ khác là trong C++ thường thì cincout mặc định làm việc với màn hình và bàn phím, còn trong Qt thì làm việc với file.

#include <QTextStream>
#include <QFile>

int main(void) {
    
  QTextStream out(stdout);

  QFile file("colours");
  
  if (!file.open(QIODevice::ReadOnly)) {
      
    qWarning("Cannot open file for reading");
    return 1;
  }

  QTextStream in(&file);

  while (!in.atEnd()) {
      
    QString line = in.readLine();    
    out << line << endl;
  }

  file.close();
}

Ví dụ trên đọc dữ liệu từ file colours. Trong đó chứa tám string.

if (!file.open(QIODevice::ReadOnly)) {
  qWarning("Cannot open file for reading");
  return 1;
}

Phương thức open() sẽ mở file ở chế độc read-only. Nếu giá trị trả về false thì ta báo không mở file được và cho thoát chương trình.

QTextStream in(&file);

Tạo một đối tượng thuộc lớp QTextStream nhận vào đối tượng QFile. Đây chính là một stream và ta sẽ dùng nó để đọc file.

while (!in.atEnd()) {
    
  QString line = in.readLine();    
  out << line << endl;
}

Trong vòng lặp while, chúng ta sẽ đọc file theo từng dòng cho đến hết file bằng phương thức readLine(), mỗi dòng trong file sẽ được lưu vô một biến QString rồi in ra màn hình. Phương thức atEnd() kiểm tra xem đã hết file hay chưa.

file.close();

Khi đã đọc file xong ta dùng phương thức close() để đóng file.

red
green
blue
yellow
brown
white
black
violet

Ghi file trong Qt

Để ghi file thì cũng tương tự như đọc file, phải mở được file đó trước. Và cũng như đọc file, chúng ta sẽ có một stream riêng để ghi file.

#include <QTextStream>
#include <QFile>

int main(void) {
    
  QTextStream out(stdout);
    
  QString filename = "distros";
  QFile file(filename);
  
  if (file.open(QIODevice::WriteOnly)) {
      
    QTextStream out(&file);
    out << "Xubuntu" << endl;
    out << "Arch" << endl;
    out << "Debian" << endl;
    out << "Redhat" << endl;
    out << "Slackware" << endl;
    
  } else {
      
    qWarning("Could not open file");
  }  
  
  file.close(); 
}

Ví dụ trên ghi tên 5 phiên bản của hệ điều hành họ Linux vào một file tên là distros.

QString filename = "distros";
QFile file(filename);

Tạo một đối tượng QFile với tên file.

if (file.open(QIODevice::WriteOnly))

Mở file ở chế độ WriteOnly với phương thức open().

QTextStream out(&file);

Dòng code trên tạo ra một đối tượng QTextStream để ghi file.

out << "Xubuntu" << endl;
out << "Arch" << endl;
out << "Debian" << endl;
out << "Redhat" << endl;
out << "Slackware" << endl;

Dùng toán tử << để ghi dữ liệu.

file.close(); 

Cuối cùng đóng file lại.

Xubuntu
Arch
Debian
Redhat
Slackware

Copy một file trong Qt

Xem ví dụ để biết cách copy file.

#include <QTextStream>
#include <QFile>

int main(int argc, char *argv[]) {
    
  QTextStream out(stdout);
  
  if (argc != 3) {
      
      qWarning("Usage: copyfile source destination");
      return 1;
  }
  
  QString source = argv[1];
  
  if (!QFile(source).exists()) {
      qWarning("The source file does not exist");
      return 1;
  }
  
  QString destin(argv[2]);

  QFile::copy(source, destin);
}

Qt cung cấp phương thức QFile::copy() để copy một file, tham số gồm file gốc và file đích sẽ được tạo ra.

if (argc != 3) {
    
    qWarning("Usage: copyfile source destination");
    return 1;
}

Ở đây mình truyền vào chương trình 2 tham số là tên file gốc và tên file đích. Nếu không đủ cả 2 thì ta báo lỗi và thoát chương trình.

QString source = argv[1];

Lấy tên file gốc từ tham số đầu tiên.

if (!QFile(source).exists()) {
    qWarning("The source file does not exist");
    return 1;
}

Kiểm tra xem file gốc có tồn tại hay không, nếu không thì in ra thông báo lỗi và thoát chương trình.

QString destin(argv[2]);

Lấy file đích.

QFile::copy(source, destin);

Dùng phương thức QFile::copy() để copy file gốc sang file đích.

Làm việc với thư mục trong Qt

Lớp QDir được sinh ra để làm việc này. Xem ví dụ.

#include <QTextStream>
#include <QDir>

int main(void) {
    
  QTextStream out(stdout);
  QDir dir;
  
  if (dir.mkdir("mydir")) {
    out << "mydir successfully created" << endl;
  }
  
  dir.mkdir("mydir2");
  
  if (dir.exists("mydir2")) {
    dir.rename("mydir2", "newdir");    
  }
  
  dir.mkpath("temp/newdir");
}

Ở trên mình trình bày bốn phương thức thường dùng của lớp này.

if (dir.mkdir("mydir")) {
  out << "mydir successfully created" << endl;
}

Phương thức mkdir() tạo ra một thư mục. Trả về true nếu tạo thành công.

if (dir.exists("mydir2")) {
  dir.rename("mydir2", "newdir");    
}

Phương thức exists() kiểm tra xem một thư mục đã tồn tại hay chưa. Phương thức rename() đổi tên một thư mục.

dir.mkpath("temp/newdir");

Phương thức mkpath() tạo ra một thư mục và tạo cả các thư mục cha của nó nếu chưa có.

Một số hằng số về các thư mục đặc biệt

Trong hệ thống có một số thư mục đặc biệt, chẳng hạn như thư mục home hay thư mục gốc. Xem ví dụ.

#include <QTextStream>
#include <QDir>

int main(void) {
    
  QTextStream out(stdout);
   
  out << "Current path:" << QDir::currentPath() << endl;
  out << "Home path:" << QDir::homePath() << endl;
  out << "Temporary path:" << QDir::tempPath() << endl;
  out << "Rooth path:" << QDir::rootPath() << endl;
}

Ví dụ trên in ra bốn thư mục đặc biệt

out << "Current path:" << QDir::currentPath() << endl;

Phương thức QDir::currentPath() trả về thư mục mà mình đang làm việc chẳng hạn ở trên mình đang làm việc với file special_paths.cpp thì phương thức này sẽ in ra đường dẫn đến file này.

out << "Home path:" << QDir::homePath() << endl;

Phương thức QDir::homePath() trả về đường dẫn đến thư mục user của bạn.

out << "Temporary path:" << QDir::tempPath() << endl;

Phương thức QDir::tempPath() trả về đường dẫn đến thư mục temp của user của bạn.

out << "Rooth path:" << QDir::rootPath() << endl;

Phương thức QDir::rootPath() trả về đường dẫn đến thư mục gốc của bạn, nếu bạn dùng Windows thì thường nó là thư mục cài win, nếu dùng linux thì là dấu /.

Đường dẫn file

Xem ví dụ.

#include <QTextStream>
#include <QFileInfo>

int main(int argc, char *argv[]) {
    
  QTextStream out(stdout);

  if (argc != 2) {
      
      out << "Usage: file_times file" << endl;
      return 1;
  }
  
  QString filename = argv[1];
  
  QFileInfo fileinfo(filename);
  
  QString absPath = fileinfo.absoluteFilePath();
  QString baseName = fileinfo.baseName();
  QString compBaseName = fileinfo.completeBaseName();
  QString fileName = fileinfo.fileName();
  QString suffix = fileinfo.suffix();
  QString compSuffix = fileinfo.completeSuffix();
  
  out << "Absolute file path: " << absPath << endl;
  out << "Base name: " << baseName << endl;
  out << "Complete base name: " << compBaseName << endl;
  out << "File name: " << fileName << endl;
  out << "Suffix: " << suffix << endl;
  out << "Whole suffix: " << compSuffix << endl;
}

Trong ví dụ trên ta in ra đường dẫn của một file và trính xuất một vài thành phần trong đường dẫn đó.

QFileInfo fileinfo(filename);

Dùng lớp QFileInfo để làm việc này.

QString absPath = fileinfo.absoluteFilePath();

Phương thức absoluteFilePath() trả về đường dẫn đầy đủ của file.

QString baseName = fileinfo.baseName();

Phương thức baseName() trả về tên file, không lấy phần mở rộng của file, không lấy đường dẫn thư mục.

QString compBaseName = fileinfo.completeBaseName();

Phương thức completeBaseName() cũng giống với phương thức baseName().

QString fileName = fileinfo.fileName();

Phương thức fileName() trả về tên file và cả phần mở rộng của file.

QString suffix = fileinfo.suffix();

Phương thức suffix() trả về phần mở rộng của file.

QString compSuffix = fileinfo.completeSuffix();

Phương thức completeSuffix() cũng giống với phương thức suffix().

Liệt kê nội dung thư mục trong Qt

Ví dụ dưới đây liệt kê nội dung của một thư mục.

#include <QTextStream>
#include <QFileInfo>
#include <QDir>

int main(int argc, char *argv[]) {
    
  QTextStream out(stdout);

  if (argc != 2) {
      
      qWarning("Usage: list_dir directory");
      return 1;
  }
  
  QString directory = argv[1];
  
  QDir dir(directory);
  
  if (!dir.exists()) {
      qWarning("The directory does not exist");
      return 1;
  }  
  
  dir.setFilter(QDir::Files | QDir::AllDirs);
  dir.setSorting(QDir::Size | QDir::Reversed);

  QFileInfoList list = dir.entryInfoList();
  
  int max_size = 0;
  
  foreach (QFileInfo finfo, list) {
      
      QString name = finfo.fileName();
      int size = name.size();
      
      if (size > max_size) {
          
          max_size = size;
      }
  }
  
  int len = max_size + 2;
  
  out << QString("Filename").leftJustified(len).append("Bytes") << endl;
  
  for (int i = 0; i < list.size(); ++i) {
      
    QFileInfo fileInfo = list.at(i);
    QString str = fileInfo.fileName().leftJustified(len);
    str.append(QString("%1").arg(fileInfo.size()));
    out << str << endl;
  }
    
  return 0;
}

Dùng phương thức entryInfoList() trong lớp QDir để làm việc này. Trong code ví dụ trên chúng ta in ra danh sách đã được sắp xếp theo kích thước của file/thư mục. Chúng ta in ra hai cột, cột đầu tiên là tên file/thư mục, cột thứ hai là kích thước file/thư mục.

QDir dir(directory);

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

dir.setFilter(QDir::Files | QDir::AllDirs);

Phương thức setFilter() có tác dụng lọc các thành phần mà mình muốn liệt kê, trong ví dụ mình lọc ra cả file và thư mục.

dir.setSorting(QDir::Size | QDir::Reversed);

Phương thức setSorting() sẽ tự động sắp xếp các thành phần trong thư mục dựa theo tham số truyền vào mà ở đây là sắp xếp đảo ngược theo kích thước.

QFileInfoList list = dir.entryInfoList();

Phương thức entryInfoList() sẽ trả về một đối tượng thuộc lớp QFileInfoList, lớp này là một danh sách các đối tượng QFileInfo đã được sắp xếp. Lớp này tương tự với QList<QFileInfo>.

foreach (QFileInfo finfo, list) {
    
    QString name = finfo.fileName();
    int size = name.size();
    
    if (size > max_size) {
        
        max_size = size;
    }
}

Duyệt qua danh sách và tìm ra file có độ dài lớn nhất. Mình lấy thông tin này ra để lát sau in kết quả ra màn hình thì có thể ngăn cách hai cột cho đẹp.

int len = max_size + 2;

Chúng ta cũng không dùng chính xác độ dài lớn nhất mà cộng thêm 2 vào nữa.

for (int i = 0; i < list.size(); ++i) {
  QFileInfo fileInfo = list.at(i);
  QString str = fileInfo.fileName().leftJustified(len);
  str.append(QString("%1").arg(fileInfo.size()));
  out << str << endl;
}

Cuối cùng ta duyệt qua toàn bộ danh sách và in kết quả ra màn hình.

Filename      Bytes
list_dir.pro  291
list_dir.cpp  1092
..            4096
.             4096
list_dir.o    10440
list_dir      19075
Makefile      28369

Trong bài này, bạn đã được học cách làm việc với file và thư mục trong Qt.

Qt 5 C++ – Một số lớp hữu ích

Trong phần này chúng ta sẽ học về một số lớp dùng để lưu trữ dữ liệu trong Qt 5. Đó là các lớp QVector, QList, QStringList, QSet, và QMap.

Trong thư viện chuẩn của C++ là Standard Template Library (STL) cũng có một số lớp có chức năng tương tự. Bạn có thể dùng các lớp đó hoặc các lớp mình sắp nói dưới đây, cái nào cũng tốt cả.

Các lớp này được chia làm hai loại, loại thứ nhất lưu dữ liệu theo kiểu danh sách, loại thứ hai lưu theo kiểu các cặp khóa-giá trị (key-value). QList, QVector, QLinkedList là các lớp lưu theo kiểu danh sách; còn QMap và QHash lưu theo kiểu cặp khóa-giá trị.

QVector

QVector lưu trữ dữ liệu theo kiểu danh sách. Các phần tử  trong QVector nằm kế tiếp nhau trong bộ nhớ và có thể truy cập thông qua chỉ số nên có tốc độ truy xuất dữ liệu rất nhanh nếu số lượng phần tử không nhiều. Nếu dữ liệu cần lưu trữ quá lớn thì tốt nhất là bạn nên dùng QList thay thế.

#include <QVector>
#include <QTextStream>

int main(void) {

    QTextStream out(stdout);

    QVector<int> vals = {1, 2, 3, 4, 5};
    
    out << "The size of the vector is: " << vals.size() << endl;
       
    out << "The first item is: " << vals.first() << endl;
    out << "The last item is: " << vals.last() << endl;
    
    vals.append(6);
    vals.prepend(0);   
    
    out << "Elements: "; 
    
    for (int val : vals) {
        out << val << " ";
    }    
    
    out << endl;
   
    return 0;
}

Ví dụ trên tạo ra một vector lưu trữ các số nguyên.

QVector<int> vals = {1, 2, 3, 4, 5};

Khởi tạo vector

out << "The size of the vector is: " << vals.size() << endl;

Phương thức size() trả về kích thước của vector – số lượng các phần tử có trong vector.

out << "The first item is: " << vals.first() << endl;

Truy xuất phần tử đầu tiên với phương thức first().

out << "The last item is: " << vals.last() << endl;

Truy xuất phần tử cuối cùng với phương thức last().

vals.append(6);

Phương thức append() chèn một phần tử vào sau vector.

vals.prepend(0);

Phương thức prepend() chèn một phần tử vào đầu vector.

for (int val : vals) {
    out << val << " ";
}    

Chúng ta duyệt qua vector và in các phần tử ra màn hình.

The size of the vector is: 5
The first item is: 1
The last item is: 5
Elements: 0 1 2 3 4 5 6 

QList

QList cũng có chức năng tương tự như QVector. Nó cũng lưu trữ dữ liệu theo kiểu danh sách và truy xuất các phần tử thông qua chỉ số, nhưng tốc độ chèn và xóa dữ liệu nhanh hơn QVector. Đây là một trong những lớp hay dùng nhất trong Qt

#include <QTextStream>
#include <QList>
#include <algorithm>

int main(void) {

    QTextStream out(stdout);

    QList<QString> authors = {"Balzac", "Tolstoy", "Gulbranssen", "London"};
  
    for (int i=0; i < authors.size(); ++i) {
        out << authors.at(i) << endl;
    }  
  
    authors << "Galsworthy" << "Sienkiewicz";
    
    out << "***********************" << endl;

    std::sort(authors.begin(), authors.end());

    out << "Sorted:" << endl;
    for (QString author : authors) {  
        out << author << endl;
    }  
}

Ví dụ cách sử dụng QList.

QList<QString> authors = {"Balzac", "Tolstoy", "Gulbranssen", "London"};

Khởi tạo QList là danh sách tên các nhà văn.

for (int i=0; i < authors.size(); ++i) {
    out << authors.at(i) << endl;
}  

Ta duyệt qua QList bằng vòng lặp for và in dữ liệu ra màn hình bằng phương thức at(). Phương thức này nhận tham số đầu vào là chỉ số phần tử.

authors << "Galsworthy" << "Sienkiewicz";

Dùng toán tử << để chèn một phần tử mới vào sau QList.

std::sort(authors.begin(), authors.end());

Phương thức std::sort() sắp xếp các phần tử trong QList.

out << "Sorted:" << endl;
for (QString author : authors) {
    out << author << endl;
} 

In ra danh sách đã được sắp xếp.

Balzac
Tolstoy
Gulbranssen
London
***********************
Sorted:
Balzac
Galsworthy
Gulbranssen
London
Sienkiewicz
Tolstoy

QStringList

QStringList cũng giống như QList nhưng chuyên để làm việc với string.

#include <QTextStream>
#include <QList>

int main(void) {

    QTextStream out(stdout);

    QString string = "coin, book, cup, pencil, clock, bookmark";
    QStringList items = string.split(",");
    QStringListIterator it(items);
    
    while (it.hasNext()) {
        out << it.next().trimmed() << endl;    
    }
}

Ví dụ trên tạo ra một danh sách các string và in các phần tử trong danh sách ra màn hình.

QString string = "coin, book, cup, pencil, clock, bookmark";
QStringList items = string.split(",");

Phương thức split() tách một chuỗi thành nhiều chuỗi con dựa vào dấu phân tách mà ta đưa vào.

QStringListIterator it(items);

QStringListIterator là một lớp chuyên dùng để thao tác với QStringList.

while (it.hasNext()) {
    out << it.next().trimmed() << endl;    
}

Sau khi đã khởi tạo iterator, chúng ta in từng phần tử ra màn hình. Phương thức trimmed() sẽ xóa các khoảng trống thừa trong chuỗi.

coin
book
cup
pencil
clock
bookmark

QSet

QSet lưu trữ các phần tử dữ liệu đơn lẻ. Các phần tử trong QSet không được lưu theo một thứ tự nào cả.

#include <QSet>
#include <QList>
#include <QTextStream>
#include <algorithm>

int main(void) {
    
    QTextStream out(stdout);

    QSet<QString> cols1 = {"yellow", "red", "blue"};
    QSet<QString> cols2 = {"blue", "pink", "orange"};
      
    out << "There are " << cols1.size() << " values in the set" << endl;
   
    cols1.insert("brown");
   
    out << "There are " << cols1.size() << " values in the set" << endl;
    
    cols1.unite(cols2);
    
    out << "There are " << cols1.size() << " values in the set" << endl;
    
    for (QString val : cols1) {
        out << val << endl;
    }
    
    QList<QString> lcols = cols1.values();
    std::sort(lcols.begin(), lcols.end());
    
    out << "*********************" << endl;
    out << "Sorted:" << endl;
    
    for (QString val : lcols) {
        out << val << endl;
    }    
   
   return 0;
}

Ví dụ trên tạo ra các QSet lưu tên của các màu. Không có phần tử nào xuất hiện hai lần trở lên – tức là giá trị của mỗi phần tử là khác nhau.

QSet<QString> cols1 = {"yellow", "red", "blue"};
QSet<QString> cols2 = {"blue", "pink", "orange"};

Chúng ta có hai tập dữ liệu, hai tập này chứa một phần tử chung là “blue”.

out << "There are " << cols1.size() << " values in the set" << endl;

Phương thức size() trả về kích thước của tập.

cols1.insert("brown");

Chúng ta thêm một phần tử mới vào tập bằng phương thức insert().

cols1.unite(cols2);

Phương thức unite() thực hiện phép hợp hai tập này. Tập cols1 sẽ thêm vào các phần tử có trong tập cols2 mà chưa có trong tập cols1, mà ở đây chính là phần tử “blue”.

for (QString val : cols1) {
    out << val << endl;
}

Ta dùng vòng lặp for để in các phần tử ra của tập cols1 màn hình.

QList<QString> lcols = cols1.values();
std::sort(lcols.begin(), lcols.end());

Chúng ta không thể sắp xếp một tập hợp. Nếu cần bạn có thể tạo ra một QList rồi sắp xếp, phương thức values() sẽ trả về một QList chứa các phần tử trong tập, nhưng thứ tự của các phần tử trong tập là ngẫu nhiên.

There are 3 values in the set
There are 4 values in the set
There are 6 values in the set
pink
orange
brown
blue
yellow
red
*********************
Sorted:
blue
brown
orange
pink
red
yellow

QMap

QMap lưu dữ liệu theo kiểu các cặp khóa-giá trị (key-value). Chúng ta có thể truy xuất phần tử thông qua key.

#include <QTextStream>
#include <QMap>

int main(void) {

    QTextStream out(stdout);

    QMap<QString, int> items = { {"coins", 5}, {"books", 3} };
    
    items.insert("bottles", 7);
    
    QList<int> values = items.values();
    
    out << "Values:" << endl;

    for (int val : values) {
        out << val << endl;
    }
    
    QList<QString> keys = items.keys();

    out << "Keys:" << endl;
    for (QString key : keys) {
        out << key << endl;
    }    
    
    QMapIterator<QString, int> it(items);
    
    out << "Pairs:" << endl;
    
    while (it.hasNext()) {
        it.next();
        out << it.key() << ": " << it.value() << endl;    
    }
}

Trong ví dụ trên, chúng ta có các cặp key, mỗi key có một value.

QMap<QString, int> items = { {"coins", 5}, {"books", 3} };

Ta tạo ra một QMap chứa hai cặp key-value.

items.insert("bottles", 7);

Ta thêm một cặp key-value bằng phương thức insert().

QList<int> values = items.values();

out << "Values:" << endl;

for (int val : values) {
    out << val << endl;
}

Chúng ta in toàn bộ các cặp ra màn hình. Ngoài ra ta có thể dùng phương thức values() để lấy về một QList chứa các value.

QList<QString> keys = items.keys();

out << "Keys:" << endl;
for (QString key : keys) {
    out << key << endl;
} 

Ta in các key ra màn hình. Cũng giống như trên, ta có thể dùng phương thức keys() để lấy về một QList các key.

QMapIterator<QString, int> it(items);

QMapIterator là biến lặp iterator cho QMap, dùng để thao tác với QMap.

while (it.hasNext()) {
    it.next();
    out << it.key() << ": " << it.value() << endl;    
}

Ta dùng iterator này để duyệt qua QMap. Phương thức key() sẽ trả về khóa và phương thức value() sẽ trả về giá trị.

Values:
3
7
5
Keys:
books
bottles
coins
Pairs:
books: 3
bottles: 7
coins: 5

Sắp xếp các lớp do người dùng định nghĩa

Trong ví dụ dưới đây, chúng ta sẽ sắp xếp một QList chứa các đối tượng thuộc lớp Book do chúng ta tự định nghĩa.

class Book {
    
    public:
        Book(QString, QString);
        QString getAuthor() const;
        QString getTitle() const;
        
    private:            
        QString author;
        QString title;    
};

Đây là file header của lớp Book.

#include <QString>
#include "book.h"

Book::Book(QString auth, QString tit) {
    
    author = auth;
    title = tit;
}

QString Book::getAuthor() const {
    
    return author;
}

QString Book::getTitle() const {
    
    return title;
}

Đây là file cài dặt các phương thức của lớp Book, trong lớp này ta cài đặt hai phương thức truy xuất dữ liệu.

#include <QTextStream>
#include <QList>
#include <algorithm> 
#include "book.h"

bool compareByTitle(const Book &b1, const Book &b2) {
    
  return b1.getTitle() < b2.getTitle();
}

int main(void) {

    QTextStream out(stdout);

    QList<Book> books = {
        Book("Jack London", "The Call of the Wild"),
        Book("Honoré de Balzac", "Father Goriot"),
        Book("Leo Tolstoy", "War and Peace"),
        Book("Gustave Flaubert", "Sentimental education"),
        Book("Guy de Maupassant", "Une vie"),
        Book("William Shakespeare", "Hamlet")
    };
    
    std::sort(books.begin(), books.end(), compareByTitle);
    
    for (Book book : books) {
        out << book.getAuthor() << ": " << book.getTitle() << endl;
    }
}

Ta tạo ra vài đối tượng Book, đưa chúng vô QList và sắp xếp với hàm std::sort.

bool compareByTitle(const Book &b1, const Book &b2) {    
  return b1.getTitle() < b2.getTitle();
}

Phương thức compareByTitle() là phương thức so sánh sẽ được sử dụng bởi hàm sort.

std::sort(oks.begin(), books.end(), compareByTitle);

Hàm std::sort sẽ sắp xếp các book dựa theo tựa book.

Honoré de Balzac: Father Goriot
William Shakespeare: Hamlet
Gustave Flaubert: Sentimental education
Jack London: The Call of the Wild
Guy de Maupassant: Une vie
Leo Tolstoy: War and Peace

Trong bài này, chúng ta đã học về một số lớp lưu trữ dữ liệu thường dùng trong Qt 5.