< Lập trình tân binh | 3.9. [Thực hành] KienTrucSuX

3.9. [Thực hành] KienTrucSuX

Tôi cho rằng kiến thức chúng ta có được trong thời gian gần đây đã đủ nhiều để có thể giúp các bạn bắt đầu viết những chương trình có ý nghĩa thiết thực hơn.

Chủ đề hôm này sẽ là viết 1 chương trình tên là KienTrucSuX.

Nếu ai từng xem Matrix thì sẽ biết The Architect (kiến trúc sư), 1 chương trình sinh mã tối cao trong thế giới Ma Trận, đứng đầu đội quân máy móc, tác giả của tất cả các phiên bản Ma Trận. Chương trình KienTrucSuX chúng ta sẽ viết trong bài học này đương nhiên sẽ không kinh khủng như vậy. Thế nhưng nó cũng có thể tự sinh ra mã nguồn cơ sở của 1 lớp C++ dựa trên các tùy chọn mà người dùng chúng ta nhập vào.

Mục tiêu

Đừng để tên của chương trình dọa sợ ! Trong thực tế, nó không phức tạp như các bạn nghĩ. Chỉ cần vận dụng linh hoạt những gì mà chúng ta từng cùng thảo luận qua thì mục tiêu đặt ra cũng sẽ không quá khó nhằn. Đây là cơ hội tốt để thử nghiệm kiến thức trong 1 ứng dụng thực thụ.

Tương tự như những bài thực hành khác trong giáo trình, chúng ta sẽ bắt đầu viết chương trình với những tính năng rất cơ bản. Tôi sẽ cho các bạn thời gian để tự viết mà, rôi chúng ta sẽ cùng nhau nghiên cứu qua 1 đoạn mã đáp án mà tôi đưa ra. Cuối cùng sẽ là 1 danh sách những tính năng có thể cải thiện thêm cho chương trình dành cho những bạn nào vẫn còn ham muốn đào sâu tìm hiểu.

Trước khi bắt đầu, chúng ta cần hiểu, chương trình KienTrucSuX của chúng ta là 1 chương trình sinh mã cho lớp C++, điều này nghĩa là gì ?

Chương trình tạo mã cho lớp C++

Chương trình kết quả sẽ là 1 ứng dụng có giao diện đồ họa người dùng, có khả năng tự động sinh mã nguồn cho 1 lớp C++ dựa theo các tùy chọn đưa ra bởi người dùng.

Tại sao lại cần 1 chương trình như vậy ? Các bạn có để ý không là tất cả các lớp trong C++ đều có cấu trúc tương tự nhau và việc lặp đi lặp lại quá trình viết đoạn mã cấu trúc đó đôi khi trở nên rất mệt mỏi ?

Hãy lấy ví dụ :

#ifndef HEADER_PHAPSU
#define HEADER_PHAPSU

class PhapSu : public NhanVat {
    public:
    PhapSu();
    ~PhapSu();

    protected:

    private:

};
#endif

Ít ra tôi thấy rằng sẽ rất hữu dụng nếu sở hữu 1 chương trình có khả năng tự động tạo ra cấu trúc cơ bản của lớp, thêm vào các từ khóa public, protected, private về quyền truy cập cũng như tạo ra sẵn 1 phương thức tạo và 1 phương thức hủy mặc định, vv…

Vì thế, chúng ta sẽ tạo ra 1 chương trình cửa sổ chứa các tùy chọn nhất định dành cho người dùng.

Cửa sổ chính của chương trình nằm bên trái. Người dùng bắt buộc phải nhập vào tên lớp muốn tạo và nếu muốn, có thể thêm cả tên của lớp mẹ mà nó kế thừa.

Chúng ta cũng đưa ra ô chọn cho các tùy chọn như “Tránh bao gồm kép các gói trong tiêu đề”. Đây là tùy chọn liên quan đến các dòng lệnh bắt đầu bằng # như #ifndef dùng để tránh việc 1 lớp bị gọi nhiều lần trong tiêu đề của lớp khác trong cùng 1 chương trình.

1 tùy chọn khác thì cho phép thêm các lời bình luận chú thích vào đầu đoạn mã với mục đích đưa ra thông tin về tác giả, ngày tạo lớp cũng như vai trò của lớp. Nhân đây, tôi cho rằng các bạn cũng nên tạo thói quen chú thích chút ít về tác dụng của lớp mỗi khi bắt đầu 1 lớp mới.

Khi chúng ta ấn vào nút “Sinh mã” nằm ở dưới cửa sổ, 1 cửa sổ mới sẽ bật ra và in ra đoạn mã nguồn tạo bởi chương trình. Chúng ta sau đó có thể cắt hoặc sao chép đoạn mã đó và dán vào IDE như Code::Block hoặc QtCreator để sử dụng.

Đây chỉ là vài tính năng để bắt đầu. Tôi sẽ gợi ý những cải tiển có thể thêm vào để khiến chương trình của chúng ta mạnh mẽ hơn. Hãy cứ bắt đầu trước với những tính năng cơ bản. Chỉ thế thôi cũng yêu cầu các bạn phải mất công suy nghĩ rồi đấy.

Vài nhắc nhở kỹ thuật

Trước khi bỏ lại các bạn bơ vơ giữa hoang đảo, tôi có 1 vài lời khuyên kỹ thuật có thể có ích giúp các bạn định hướng khi viết chương trình.

Cấu trúc chương trình

Theo kinh nghiệm thì các bạn nên tạo lớp cho từng loại cửa sổ. Bởi vì chúng ta có 2 loại, cộng thêm việc là hàm main() luôn nằm độc lập riêng 1 góc, chúng ta sẽ có tổng cộng 5 tệp mã nguồn :

  • main.cpp : chứa hàm main(), nơi khởi động chương trình và mở ra cửa sổ chính.
  • CuaSoChinh.h : tiêu đề của lớp cửa sổ chính
  • CuaSoChinh.cpp : mã xử lý của lớp cửa sổ chính
  • CuaSoMa.h : tiêu đề của lớp cửa sổ phụ, nơi chứa đoạn mã sinh ra bởi chương trình.
  • CuaSoMa.cpp : mã xử lý của lớp cửa sổ mã.

Dùng cho cửa sổ chính, chúng ta có thể kế thừa lớp QWidget như mọi khi. Đây là 1 lựa chọn không tồi.

Với cửa sổ mã, tôi đề nghị chúng ta nên kế thừa lớp QDialog. Cửa sổ chính có thể dễ dàng gọi ra cửa sổ mã nhờ phương thức exec() của cửa sổ mã.

Cửa sổ chính

Chúng ta nên tận dụng tối đa các lớp sắp xếp. Như các bạn có thể thấy từ ảnh chụp màn hình, lớp sắp xếp chính được sử dụng là sắp xếp dọc, bên trong đó lá các nhóm lựa chọn QGroupBox. Bên trong các QGroupBox lại là các lớp sắp xếp khác, thay đổi tùy theo quan điểm thẩm mỹ.

Bên trong nhóm “Thêm chú thích”, chúng ta cần thêm 1 ô chọn. Nếu ô chọn này được đánh dấu thì chúng ta sẽ thêm lời chú thích vào đoạn mã nguồn. Hãy đọc lại về cách sử dụng của các ô chọn trong QGroupBox.

Với trường “Ngày tạo”, các bạn có thể dùng QDateEdit. Chúng ta vẫn chưa thảo luận về widget này trong bài học trước nhưng nó không quá phức tạp. Thật ra nó khá giống với lớp QSpinBox, tôi tin tưởng các bạn sẽ không gặp quá nhiều khó khăn với nó.

Dễ dàng nhất là xây dựng và thêm từng thành phần của cửa sổ vào trong phương thức khởi tạo của CuaSoChinh. Tốt nhất là hãy định nghĩa các trường trong cửa sổ như các thuộc tính của lớp CuaSoChinh để cho tất cả các phương thức của lớp đều có thể truy cập đến giá trị của chúng.

Khi nút “Sinh mã” được ấn, hãy gọi 1 slot tự tạo. Trong slot tự tạo đó (thật ra chính là phương thức của CuaSoChinh), chúng ta sẽ lấy giá trị của tất cả các trường trong cửa sổ và dựa vào đó để tạo ra đoạn mã nguồn tương ứng (kiểu QString chẳng hạn). Ở đây thì chúng ta sẽ cần bỏ ra 1 ít chất xám để có thể viết ra đoạn mã đó nhưng không có gì là quá khó khăn cả.

Sau khi đoạn mã kiểu QString đã được tạo ra, slot của chúng ta sẽ gọi phương thức exec() của 1 đối tượng thuộc lớp CuaSoMa mà chúng ta đã tạo ra trước đó. Bên trong cửa sổ mã sẽ là đoạn mã kết quả của chương trình.

Cửa sổ mã

Còn đơn giản hơn lớp trước, lớp này chỉ là 1 cửa sổ trong đó có chứa 1 trường QTextEdit và 1 chiếc nút để đóng cửa sổ.

Với trường QTextEdit, hãy sử dụng 1 phong cách chữ mà các bạn cho rằng là hợp nhất với phong cách chữ viết mã nguồn. Chúng ta cũng có thể tùy chỉnh trường dùng thuộc tính readOnly để khiến người dùng chỉ có thể sao chép chứ không thể sửa được đoạn mã.

Hãy kết nối nút “Đóng” với 1 slot đặc biệt của QDialog dùng để đóng cửa sổ này và thông báo là mọi chuyện diễn ra tốt đẹp. Xin mời các bạn hãy nghiên cứu tài liệu của lớp này để biết thêm về slot tôi vừa nói.

? Vậy làm sao tôi có thể hiển thị đoạn mã được sinh ra trong CuaSoChinh trong CuaSoMa ?

Giải pháp có thể là truyền đối tượng QString này như là tham số khởi tạo cửa sổ mã. Cửa sổ mã sẽ chỉ cần hiển thị nó.

Vậy là đủ để các bạn bắt đầu rồi. Có lẽ chúng ta sẽ phải lật đi lật lại nhiều lần tài liệu của các lớp khác nhau của Qt trước khi có thể hoàn thiện chương trình. Vậy nên đừng ngại đặt nó ngay trong tầm mắt các bạn.

Chúng ta sẽ tạm chia tay 1 lúc để các bạn có thời gian suy nghĩ và viết mã rồi cùng gặp lại ở phần đáp án. Tạm biệt !

Đáp án

Reeenggggg ! Mời các bạn bỏ bàn phím xuống và chúng ta thu bài J

Mặc dù bên trên tôi đã đưa ra 1 số lời khuyên nhưng vẫn cố tình chừa lại rất nhiều chi tiết cho các bạn tự quyết định theo ý mình. Nói vậy để các bạn biết rằng, đáp án dưới đây không phải là đáp án duy nhất, càng không phải là đáp án tốt nhất cho bài thực hành. Mà thật ra cũng không tồn tại cái gọi là đáp án tốt nhất. Vậy nên, nếu đoạn mã các bạn viết có khác biệt với những gì tôi trình bày dưới đây thì cũng không có gì nghiêm trọng cả. Kể cả có bạn nào không thể tự hoàn thành được bài tập này thì cũng không cần hoảng loạn. Hãy đọc đoạn mã của tôi và tìm hiểu nó. Rồi các bạn sẽ nhận ra là sau đấy mình cũng có thể tự hoàn thành bài thực hành mà không cần nhìn lại đáp án nữa.

main.cpp

Nội dung tệp này rất đơn giản, không cần giải thích gì nhiều vì chúng ta đã nhắc đến nó hàng tỉ lần rồi.

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

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

    CuaSoChinh cuaSo;
    cuaSo.show();

    return app.exec();
}
CuaSoChinh.h

Cửa sổ chính sẽ kế thừa lớp QWidget như đã nói. Nó sẽ cần dùng đến tiền xử lý Q_OBJECT bởi chúng ta sẽ tự định nghĩa 1 slot để sử dụng.

#ifndef CUASOCHINH_H
#define CUASOCHINH_H

#include <QtWidgets>
class CuaSoChinh : public QWidget {
    Q_OBJECT

    public:
    CuaSoChinh();

    private slots:
    void sinhMa();

    private:
    QLineEdit *tenLop;
    QLineEdit *tenLopMe;
    QCheckBox *quyenTruyCap;
    QCheckBox *hamKhoiTao;
    QCheckBox *hamHuy;
    QGroupBox *chuThich;
    QLineEdit *tacGia;
    QDateEdit *ngay;
    QTextEdit *mucDich;
    QPushButton *nutSinhMa;
    QPushButton *thoat;
};
#endif // CUASOCHINH_H

Thú vị là tất cả các trường thông tin trong bản khai mẫu đều là các thuộc tính với quyền truy cập private của lớp. Chúng ta sẽ phải khởi tạo chúng trong hàm khởi tạo. Lợi ích của việc tất cả các trường đều là các thuộc tính là tất cả các phương thức của lớp đều có thể truy cập tới chúng để dùng trong quá trình sinh mã.

Lớp của chúng ta bao gồm 2 phương thức :

  • CuaSoChinh() : phương thức khởi tạo cho phép chúng ta khởi tạo tất cả các trường của cửa sổ, tạo ra các lớp sắp xếp và thêm các widget vào trong đó. Đây cũng là nơi chúng ta tùy chỉnh các chi tiết cho cửa sổ như kích thước, tiêu đề, vv…
  • sinhMa() : là phương thức (cũng đồng thời là 1 slot) mà chúng ta sẽ liên kết với tín hiệu “nút Sinh mã đã được bấm”. Khi tín hiệu này được phát ra thì slot chúng ta tạo ra sẽ được gọi bởi hệ thống.

Ở đây, tôi đã cho slot quyền truy cập private vì thấy các lớp khác không cần thiết phải sử dụng đến nó nhưng tôi cũng có thể trao quyền public nếu tôi muốn, không có vấn đề gì cả.

CuaSoChinh.cpp

Đây có thể nói là phần quan trọng nhất. Chỉ có 2 phương thức nhưng chúng lại khá đồ sộ.

Để tránh làm rối mắt, tôi sẽ chỉ trình bày ở dưới đây những gì mà tôi định làm trong đoạn mã dưới dạng các chú thích thay vì viết hẳn các câu lệnh. Về phần đáp án hoàn chỉnh, các bạn có thể tải về xem thêm nhờ đường dẫn ở cuối bài.

#include "CuaSoChinh.h"
#include "CuaSoMa.h"

CuaSoChinh::CuaSoChinh() {
    // Tao ra cac lop sap xep va cac widget
    // …

    // Ket noi cac tin hieu va cac slot
    connect(thoat, SIGNAL(clicked()), qApp, SLOT(quit()));
    connect(nutSinhMa, SIGNAL(clicked()), this, SLOT(sinhMa()));
}

void CuaSoChinh::sinhMa() {
    // Kiem tra xem nguoi dung da nhap ten lop chua, neu chua thi ket thuc ham
    if (tenLop->text().isEmpty()) {
        QMessageBox::critical(this, "Thông báo lỗi", "Xin mời nhập tên lớp bạn muốn tạo !");
        return; // Ket thuc
    }

    // Neu moi thu deu on, thuc hien sinh ma
    QString maNguon;

    // Thuc hien tao ma nguon dua tren gia tri cac truong nhap vao tu cua so
    // …

    // Tao ra cua so ma voi tham so la doan ma vua tao ra va hien thi cua so
    CuaSoMa *cuaSoMa= new CuaSoMa (maNguon, this);
    cuaSoMa->exec();
}

! Hãy để ý là tôi sử dụng trực tiếp phương thức connect() thay vì QObject::connect(). Trong thực tế, nếu 1 lớp kế thừa từ QObject, vậy thì chúng ta không cần thêm tiền tố QObject:: trước tên phương thức.

Phương thức khởi tạo không có gì quá phức tạp, chỉ cần tạo ra các widget và sắp xếp chúng vào trong cửa sổ. Phương thức sinhMa() thì đòi hòi nhiều chất xám của chúng ta hơn bởi đây là nơi thực sự thực hiện công việc tạo ra đoạn mã. Phương thức này truy cập vào giá trị các trường của cửa sổ để tạo ra đoạn mã tương ứng.

Đối tượng maNguon kiểu QString được tạo ra tùy theo các lựa chọn của người dùng. Chương trình dừng lại và 1 thông báo lỗi sẽ hiện ra nếu người dùng không nhập vào tên của lớp muốn tạo.

Ở cuối phương thức sinhMa(), chúng ta chỉ việc tạo ra cửa sổ hiển thị mã và gửi đoạn mã nguồn vừa sinh cho nó.

    CuaSoMa *cuaSoMa= new CuaSoMa (maNguon, this);
    cuaSoMa->exec();

Đoạn mã vừa sinh được truyền làm tham số khởi tạo cửa sổ mã. Cửa sổ này sẽ hiện ra khi phương thức exec() được gọi.

CuaSoMa.h

Cửa sổ mã thậm chí còn đơn giản hơn cửa sổ chính nhiều.

#ifndef CUASOMA_H
#define CUASOMA_H

#include <QtWidgets>
class CuaSoMa : public QDialog {
    public:
    CuaSoMa(QString &maNguon, QWidget *cuaSoMe);

    private:
    QTextEdit *m_maNguon;
    QPushButton *thoat;
};

#endif // CUASOMA_H

Chỉ đơn giản là 1 phương thức khởi tạo và 2 nút bấm nhỏ.

CuaSoMa.cpp

Phương thức tạo nhận vào 2 tham số :

  • Tham chiếu tới đối tượng QString chứa đoạn mã vừa tạo ra.
  • Con trỏ tới cửa sổ mẹ.
#include "CuaSoMa.h"
CuaSoMa::CuaSoMa (QString &maNguon, QWidget *cuaSoMe = 0) : QDialog(cuaSoMe){
    m_maNguon = new QTextEdit();
    m_maNguon ->setPlainText(maNguon);
    m_maNguon ->setReadOnly(true);
    m_maNguon ->setFont(QFont("Courier"));
    m_maNguon ->setLineWrapMode(QTextEdit::NoWrap);

    thoat = new QPushButton("Thoát");

    QVBoxLayout *lopChinh = new QVBoxLayout;
    lopChinh ->addWidget(m_maNguon);
    lopChinh ->addWidget(thoat);
    resize(350, 450);
    setLayout(lopChinh);

    connect(thoat, SIGNAL(clicked()), this, SLOT(accept()));
}

Nhắc lại chút kiến thức, ở đây thì tham số cuaSoMe đã được truyền từ phương thức tạo của lớp con sang phương thức tạo của lớp mẹ là QDialog.

CuaSoMa::CuaSoMa (QString &maNguon, QWidget *cuaSoMe = 0) : QDialog(cuaSoMe)

Việc truyền tham số này được thực hiện tự động nhờ tên của tham số.

Tải dự án mẫu

Dưới đây là tệp nén chứa mã nguồn của dự án.

 Tải mã nguồn KienTrucSuX

Bên trong tệp nén này chứa :

  • Các tệp mã nguồn .cpp.h
  • Tệp .pro dùng cho QtCreator
  • Tệp thực thi Windows. Nếu muốn chạy chương trình trực tiếp từ đây thì các bạn cần thêm các tệp DLL còn thiếu vào thư mục này.
Những ý tưởng cải tiến chương trình

Đừng bảo với tôi là các bạn đã hài lòng rồi nhé !

1 bài thực hành như thế này xứng đáng được cải tiến để sử dụng thường xuyên khi chúng ta viết mã. Dưới đây là 1 số cải tiến mà chúng ta có thể thêm vào cho KienTrucSuX. Chúng chỉ là ý kiến của cá nhân tôi nên nếu các bạn có ý tưởng khác, đừng ngại thử nghiệm chúng.

  • Khi ô chọn “Tránh bao gồm kép các gói trong tiêu đề” được đánh dấu, 1 dòng lệnh define sẽ được tạo ra. Mặc định thì dòng này sẽ có dạng HEADER_TENLOP. Vậy tại sao không thử hiển thị giá trị của nó. Giá trị này sẽ thay đổi đồng thời theo sự thay đổi của tên lớp. Thậm chí chúng ta có thể biến nó thành 1 trường QLineEdit để người dùng có thể thay đổi theo sở thích của họ. Mục đích của việc này là các bạn luyện tạo thao tác với tín hiệu và slot.
  • Thêm các tùy chọn khác vào quá trình sinh mã. Ví dụ chúng ta có thể thêm tùy chọn cho phép thêm các thông tin vào phần chú thích, vv…
  • Hiện tại thì chương trình chỉ sinh ra mã nguồn tệp .h. Sẽ thật tốt nếu nó có thể tự tạo cả mã nguồn cấu trúc của tệp .cpp nữa. Chúng ta có thể trình bày chúng trong 2 thẻ khác nhau sử dụng QTabWidget bên trong cửa sổ mã.
  • Chúng ta mới chỉ có thể cắt hoặc sao chép đoạn mã từ cửa sổ mã. Tại sao không thêm tính năng cho phép tải nội dung này và lưu thành tệp trên máy tính. Hãy thử thêm 1 nút bấm cho phép thực hiện tính năng này trong cửa sổ mã. Khi bấm nút này, 1 cửa sổ lưu tệp sẽ mở ra với tên tệp được gợi ý sẵn. Để làm việc này thì các bạn sẽ cần xem thêm về lớp QFile. Chúc may mắn !
  • Chương trình mới chỉ kiểm tra xem tên lớp có trống không nhưng lại chưa kiểm tra xem nó có chứa ký tự không hợp lệ không. Để thực hiện kiểm tra này, các bạn có 2 lựa chọn : sử dụng inputMask() hoặc validator(). inputMask() thì đơn giản hơn 1 chút nhưng nếu có thể thì hãy thực hành cả 2. Để biết thêm chi tiết, xin mời đọc tài liệu của QLineEdit.

Trên đây chỉ là 1 vài hướng đi nho nhỏ nhưng cũng đủ cho các bạn mất ngủ vài đêm rồi :D.

Nếu có gì không hiểu, đừng ngại để lại 1 bình luận và mọi người sẽ cố trả lời giúp bạn.