รูปปกบทความ Workshop 1 สร้างโปรแกรม Text Editor (Part 2)

1. 🎯 ตอนที่ 12: Workshop 1 - สร้างโปรแกรม Text Editor (Part 2: การอ่าน/เขียนไฟล์)

2. 📖 เปิดฉาก (The Hook)

สวัสดีครับน้องๆ สถาปนิกซอฟต์แวร์ทุกคน! กลับมาลุยกันต่อในซีรีส์ ลุยโปรเจกต์ Cross-Platform GUI ด้วย C++ และ Qt ครับ

ในตอนที่แล้ว เราได้ขึ้นโครงสถาปัตยกรรมหน้าจอ (UI Design) ของโปรแกรม Text Editor กันไปแล้ว แต่โปรแกรมพิมพ์ข้อความที่บันทึกไฟล์ไม่ได้ มันก็ไม่ต่างอะไรกับกระดาษทดที่เขียนเสร็จแล้วก็ต้องทิ้งไปใช่ไหมครับ?

ถ้าย้อนกลับไปในยุค C/C++ ดั้งเดิม เวลาที่เราต้องจัดการกับไฟล์ (File I/O) เราต้องปวดหัวกับฟังก์ชันอย่าง fopen() หรือ std::fstream ที่ต้องมานั่งจัดการเรื่องการเข้ารหัสตัวอักษร (Encoding) หรือแม้แต่ปัญหาโลกแตกว่าการขึ้นบรรทัดใหม่ของ Windows ต้องใช้ \r\n แต่ฝั่ง Linux/Mac ใช้แค่ \n ซึ่งถ้าจัดการไม่ดี ไฟล์ที่เซฟออกมาก็จะกลายเป็นภาษาต่างดาว หรือบรรทัดติดกันเป็นพรืด!

แต่โชคดีที่โลกนี้มี Qt ครับ! สถาปนิกของ Qt ได้เตรียมชุดเครื่องมือที่ทรงพลังอย่าง QFile และ QTextStream เอาไว้ให้เราแล้ว มันสามารถจัดการความซับซ้อนเหล่านี้ให้เราโดยอัตโนมัติ วันนี้เราจะมาเจาะลึกการใช้ 2 คลาสนี้เพื่อเขียนลอจิกการเปิด (Open), บันทึก (Save), และบันทึกเป็น (Save As) ให้กับ Text Editor ของเรากันครับ!

3. 🧠 แก่นวิชา (Core Concepts)

ในโลกของ Qt การอ่านหรือเขียนไฟล์ข้อความ (Plain Text) มีกระบวนการทำงานที่แบ่งหน้าที่กันอย่างชัดเจนตามหลักการของ Object-Oriented Design ครับ:

  • QFile (ท่อส่งข้อมูล): ทำหน้าที่เป็นตัวแทนของ “ไฟล์ทางกายภาพ” บนฮาร์ดดิสก์ รับผิดชอบเรื่องการเชื่อมต่อ (IO Device) โดยตรง เช่น การสั่ง open() และ close() ไฟล์ รวมถึงกำหนดโหมดการเปิด (Open Mode) เช่น QIODevice::ReadOnly (อ่านอย่างเดียว) หรือ QIODevice::WriteOnly (เขียนอย่างเดียว).
  • QTextStream (ล่ามแปลภาษา): เนื่องจาก QFile มองทุกอย่างเป็นแค่ข้อมูลไบนารี (ก้อน Byte) ดิบๆ เราจึงต้องนำ QTextStream มาครอบ QFile เอาไว้อีกชั้นหนึ่ง. เปรียบเสมือน “ล่าม” ที่คอยรับข้อความ (String) จากโปรแกรมเรา แล้วแปลงเป็นการเข้ารหัสที่ถูกต้องก่อนส่งลงท่อ QFile การใช้ Text Stream ทำให้เราสามารถใช้เครื่องหมาย << หรือ >> ได้เหมือนกับ std::cout ของ C++ เลยครับ.

ความแตกต่างระหว่าง Save และ Save As ในการทำโปรแกรม Text Editor ลอจิกของการบันทึกไฟล์จะมี 2 กรณีครับ:

  1. Save As (บันทึกเป็น): เราจะต้องเรียกหน้าต่าง QFileDialog เพื่อให้ผู้ใช้เลือกพาธ (Path) และตั้งชื่อไฟล์ใหม่ จากนั้นจึงทำการเขียนข้อมูลลงไป.
  2. Save (บันทึก): เราต้องเช็กก่อนว่า “ไฟล์นี้เคยถูกบันทึกไปหรือยัง?” (อาจใช้ตัวแปรแบบ bool isUntitled มาคอยดัก) ถ้ายังไม่เคยบันทึก ให้โยนการทำงานไปที่ฟังก์ชัน “Save As” แต่ถ้าเคยบันทึกแล้ว (มีชื่อไฟล์อยู่ในระบบ) ก็ให้เขียนทับไฟล์เดิมได้เลยทันที.
แผนภาพการเชื่อมต่อระหว่าง QTextStream และ QFile

4. 💻 ร่ายมนต์โค้ด (Show me the Code)

เรามาดูวิธีการนำทฤษฎีมาเขียนเป็น C++ กันครับ (โค้ดนี้จะอยู่ใน mainwindow.cpp ต่อจากตอนที่แล้ว) เราจะประกาศตัวแปร QString currentFile; เอาไว้เก็บชื่อไฟล์ปัจจุบันด้วยนะครับ

1. การเปิดไฟล์ (Open File)

#include <QFile>
#include <QTextStream>
#include <QFileDialog>
#include <QMessageBox>

void MainWindow::on_actionOpen_triggered()
{
    // 1. เรียก Dialog เลือกไฟล์
    QString fileName = QFileDialog::getOpenFileName(this, "เปิดไฟล์", "", "Text Files (*.txt);;All Files (*.*)");
    if (fileName.isEmpty()) return; // ถ้าผู้ใช้กด Cancel ให้จบการทำงาน

    // 2. ใช้ QFile เปิดไฟล์แบบอ่านอย่างเดียว และโหมดข้อความ
    QFile file(fileName);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        QMessageBox::warning(this, "Error", "ไม่สามารถเปิดไฟล์ได้!");
        return;
    }

    // 3. ใช้ QTextStream เป็นล่ามอ่านข้อมูล
    QTextStream in(&file);
    in.setAutoDetectUnicode(true); // ป้องกันปัญหาภาษาไทยกลายเป็นต่างดาว (乱码)

    // 4. อ่านเนื้อหาทั้งหมดมาใส่ใน QPlainTextEdit (สมมติชื่อ textEdit)
    ui->textEdit->setPlainText(in.readAll());
    
    // 5. ปิดไฟล์เสมอเมื่อทำงานเสร็จ!
    file.close();
    
    // เก็บชื่อไฟล์ไว้ใช้ตอนกด Save ปกติ
    currentFile = fileName; 
    setWindowTitle(currentFile + " - WP Text Editor");
}

2. การบันทึกเป็น (Save As)

void MainWindow::on_actionSaveAs_triggered()
{
    // 1. เลือกที่เก็บและตั้งชื่อไฟล์
    QString fileName = QFileDialog::getSaveFileName(this, "บันทึกเป็น", "", "Text Files (*.txt);;All Files (*.*)");
    if (fileName.isEmpty()) return;

    // 2. โยนชื่อไฟล์ไปให้ฟังก์ชัน Save ทำงานต่อ
    saveFile(fileName);
}

3. การบันทึก (Save) แบบชาญฉลาด

void MainWindow::on_actionSave_triggered()
{
    // ถ้ายังไม่มีชื่อไฟล์ (เป็นไฟล์ใหม่) ให้ไปเรียก Save As แทน
    if (currentFile.isEmpty()) {
        on_actionSaveAs_triggered();
    } else {
        // ถ้ามีชื่อไฟล์อยู่แล้ว ก็บันทึกทับไฟล์เดิมได้เลย
        saveFile(currentFile);
    }
}

// ฟังก์ชันแกนหลักสำหรับเขียนไฟล์ลงฮาร์ดดิสก์
void MainWindow::saveFile(const QString &fileName)
{
    QFile file(fileName);
    // เปิดแบบเขียนอย่างเดียว (ถ้ามีไฟล์อยู่แล้วจะถูกเคลียร์เนื้อหาเดิมทิ้งอัตโนมัติจาก WriteOnly)
    if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
        QMessageBox::warning(this, "Error", "ไม่สามารถบันทึกไฟล์ได้!");
        return;
    }

    QTextStream out(&file);
    out.setAutoDetectUnicode(true); // เซตให้รองรับ Unicode

    // ดึงข้อความจากหน้าจอ ส่งเข้าไปใน Stream ผ่าน Operator <<
    QString text = ui->textEdit->toPlainText();
    out << text;

    // ปิดไฟล์และอัปเดตสถานะ
    file.close();
    currentFile = fileName;
    setWindowTitle(currentFile + " - WP Text Editor");
}

5. 🛡️ เคล็ดลับจากคัมภีร์ลับ (Under the Hood / Pro-Tips)

ในฐานะ Senior พี่ขอหยิบหลุมพรางที่โปรแกรมเมอร์ C++ มือใหม่มักจะพลาดเวลาทำ File I/O มาเตือนกันครับ:

  1. จงใช้ QIODevice::Text เสมอเวลาอ่าน/เขียนไฟล์ข้อความ: ถ้าน้องๆ สังเกต พี่จะใส่ Flag ตัวนี้ไว้เสมอตอนเรียก file.open(). เวทมนตร์ของมันคือ หากเรารันโปรแกรมนี้บน Windows เวลามันเซฟไฟล์ มันจะแอบเปลี่ยนเครื่องหมายการขึ้นบรรทัดใหม่ \n ให้กลายเป็น \r\n โดยอัตโนมัติ! และเวลาอ่านกลับมา มันก็จะยุบ \r\n กลับเป็น \n ให้เราจัดการต่อใน C++ ได้อย่างง่ายดาย ทำให้ไฟล์ของเราเปิดอ่านข้าม OS (Mac/Linux/Windows) ได้อย่างเนียนกริบครับ.
  2. อย่าลืม file.close() เด็ดขาด!: การเขียนข้อมูลลง QFile ไม่ได้หมายความว่าข้อมูลจะถูกเขียนลงฮาร์ดดิสก์ทันทีนะครับ มันอาจจะยังค้างอยู่ใน Buffer (Memory) การเรียกคำสั่ง close() นอกจากจะเป็นการคืนทรัพยากร (File Handle) ให้กับระบบปฏิบัติการแล้ว มันยังเป็นการบังคับให้ข้อมูลใน Buffer ถูกเขียน (Flush) ลงไฟล์จนเสร็จสมบูรณ์ด้วย หากลืมปิด ข้อมูลของน้องๆ อาจจะหายวับไปกับตานะครับ!.
  3. เกราะป้องกันภาษาต่างดาวด้วย setAutoDetectUnicode: หากไฟล์ของน้องๆ มีภาษาไทย จีน ญี่ปุ่น หรือข้อความอีโมจิ การใช้คำสั่ง setAutoDetectUnicode(true) ให้กับ QTextStream จะช่วยให้ Qt ตรวจจับและใช้การเข้ารหัสแบบ Unicode ได้อย่างถูกต้อง. หมดปัญหากับคำว่า “อ่านไฟล์มาแล้วกลายเป็นตัวสี่เหลี่ยม หรือเครื่องหมายคำถาม” อย่างสิ้นเชิงครับ.

6. 🏁 บทสรุป (To be continued…)

จบกันไปแล้วครับสำหรับ Workshop การสร้าง Text Editor ที่สมบูรณ์แบบ ตอนนี้โปรแกรมของเราไม่เพียงแค่มีหน้าตาที่สวยงามจากตอนที่แล้ว แต่ยังมี “สมอง” ที่สามารถโต้ตอบ สื่อสาร และจัดเก็บข้อมูลลงฮาร์ดดิสก์ (File I/O) ได้อย่างแข็งแกร่งด้วยพลังของ QFile และ QTextStream ครับ!

น้องๆ ลองเอาโค้ดชุดนี้ไปประกอบร่างเข้ากับโปรเจกต์เดิม แล้วทดลองคอมไพล์รันดูนะครับ ลองพิมพ์ภาษาไทยแล้วเซฟ จากนั้นลองเปิดกลับมาดู ความรู้สึกตอนที่โปรแกรมของเราสามารถทำงานได้จริงบนโลกใบนี้ มันเป็นความภาคภูมิใจที่สุดของคนเป็นโปรแกรมเมอร์เลยล่ะครับ! ในตอนต่อไป เราจะเข้าสู่ระบบที่ซับซ้อนขึ้นอีกนิด นั่นคือโลกของการจัดการข้อมูลด้วย Model/View Architecture ครับ รอติดตามกันได้เลย!


ต้องการที่ปรึกษาด้านการออกแบบสถาปัตยกรรมซอฟต์แวร์ C++ หรือระบบ Cross-Platform GUI ให้กับองค์กรของคุณ? ทีมงาน WP Solution พร้อมให้บริการออกแบบและพัฒนาซอฟต์แวร์แบบครบวงจร ดูรายละเอียดบริการของเราได้ที่: www.wpsolution2017.com หรือพูดคุยปรึกษาเบื้องต้นได้ที่ Line: wisit.p