Một Text Editor cơ bản thì cần có các chức năng chính như: nhập ký tự, lưu văn bản, undo, redo…Để giải quyết bài toán trên, nhóm 7 chúng em đưa ra các hướng giải quyết sau cho mỗi yêu cầ
Trang 1TRƯỜNG ĐẠI HỌC BÁCH KHOA HÀ NỘI
VIỆN ĐIỆN TỬ VIỄN THÔNG
Bộ môn Cấu trúc dữ liệu và Giải thuật
- -BÀI TẬP LỚN SỐ 7
Xây dựng một phần mềm soạn thảo TEXT EDITOR
Giảng viên hướng dẫn: TS Đỗ Thị Ngọc Diệp
Sinh viên thực hiện: Phạm Bá Độ – 20203362
Vũ Xuân Thịnh – 20203596
Lớp: 133323 – ET2100
Hà Nội, 7 – 2022
Trang 2Bảng danh sách nhóm,công việc được phân công
STT Họ và
Tên
MSSV
Công việc Mức độ
hoàn tành
Ghi ch ú
Xuân
THỊNH
20203596 Thinh.vx203596
@sis.hust.edu.vn
Thiết kế, cài đặt giao diện và các
kỹ thuật liên quan đến chương trình.
Đưa thuật toán Undo/
Redo vào kết hợp trong thuật toán giao diện.
100%
Bá ĐỘ 20203362
do.pb20203362
@sis.hust.edu.vn
Thiết kế và cài đặt thư viện Stack.
Thiết kế và cài đặt thuật toán Undo/Redo.
100%
Trang 3I.Giới thiệu bài toán
Bài toán: “Xây dựng một phần mềm soạn thảo text editor, có giao diện, cho phép nhập ký tự vào từ bàn phím Cho phép thực hiện chức năng
undo/redo việc nhập ký tự tối đa 10 ký tự”
Text Editor hay Trình soạn thảo văn bản là một trong những công cụ cơ bản giúp mọi người soạn thảo văn bản và lưu văn bản đó Một số ví dụ điển hình của một Text Editor như Microsoft Word, NotePad, Pages… Một Text Editor cơ bản thì cần có các chức năng chính như: nhập ký tự, lưu văn bản, undo, redo…
Để giải quyết bài toán trên, nhóm 7 chúng em đưa ra các hướng giải quyết sau cho mỗi yêu cầu:
Giao diện: sử dụng Win32 API và ngôn ngữ C++ để tạo giao diện và các chức năng liên quan
Chức năng undo/redo: Sử dụng cấu trúc dữ liệu Stack để lưu các văn bản dành cho hai chức năng trên
Định dạng văn bản: văn bản sẽ được lưu với đuôi ‘.txt’
Win32 API là bộ giao diện lập trình ứng dụng (API) cốt lõi của Microsoft có sẵn trong các hệ điều hành Microsoft Windows Để sử dụng bộ giao diện này, chúng em sử dụng ngôn ngữ lập trình C++ và thư viện ‘window.h’ Vì đặc thù của các hàm xây dựng giao diện khá phức tạp và không nằm trong các nội dung chính của bản báo cáo nên chúng em sẽ không trình bày sâu
về phần lập trình giao diện, tuy nhiên các thuật toán Undo/Redo sẽ được phối hợp với các hàm giao diện để có thể tạo ra một chương trình hoàn chỉnh Vì thế khi trình bày cài đặt thuật toán sẽ có những chi tiết thuộc về mặt xử lý giao diện sẽ được ghi chú
Sau đây là cấu trúc chung cho cả chương trình:
Trang 4Hình 1
No
No
Yes
Yes
Yes
User thao tác làm thay đổi văn bản
Help
Mở file sẵn Soạn văn bản mới
Mở Text Editor Bắt đầu
User muốn save
Save file
User muốn exit
Xử lý undo/redo
Stop
Trang 5III Mô tả cấu trúc dữ liệu sử dụng
Trong chương trình này, có hai thuật toán quan trọng mà nhóm em chú trọng đó là Undo và Redo Và để có thể sử dụng hai chức năng Undo và Redo thì cần lưu các thay đổi của văn bản, và khi User muốn Undo hay Redo một công việc, thì công việc trước đó sẽ được hoàn lại Vì thế để lưu các công việc này lại theo đúng thứ tự công việc muộn nhất sẽ là công việc được hoàn lại sớm nhất, nhóm em quyết định sử dụng cấu trúc Stack (ngăn xếp)
Stack là một cấu trúc dữ liệu hoạt động theo nguyên tắc Last In First Out, phù hợp với cách lưu dữ liệu mà nhóm em muốn Stack hay Ngăn Xếp hiểu đơn giản là một cấu trúc dữ liệu mà phần tử mới khi được thêm vào sẽ được thêm vào stack và khi lấy ra ta sẽ lấy phần tử ở đỉnh stack (phần tử được thêm vào gần nhất)
Một ví dụ trong thực tế của stack giúp chúng ta có thể dễ hình dung được
đó chính là hộp trụ tròn để đựng các miếng khoai tây chiên tròn Khi muốn cho khoai tây chiên vào trong hộp, ta sẽ cho khoai vào từ đáy hộp cầu đi lên
và khi muốn lấy khoai tây chiên ra thì ta sẽ lấy miếng khoai trên cùng, đó là miếng khoai được bỏ vào cuối cùng
Để có thể sử dụng được cấu trúc này, em tạo riêng thư viện Stack, bao gồm tất cả các hàm khởi tạo và và các hàm để làm việc với Stack Đối với cấu trúc lưu trữ của Stack, nhóm em quyết định sử dụng Singly Linked List (Danh sách liên kết đơn) để lưu các phần tử Mỗi Node của Singly Linked List bao gồm 2 phần: dữ liệu và con trỏ chứa địa chỉ của Node tiếp theo Linked list này sẽ được xây dựng các hàm ra vào để có thể trở thành một Stack Đặc biệt dữ liệu của Node có dạng String của C++ bởi vì văn bản là chuỗi các kỳ tự, vì vậy việc lưu văn bản cho hai chức năng Undo và Redo dưới dạng chuỗi ký tự là cách đơn giản nhất
Dưới đây là cấu trúc đơn giản của một Stack sửu dụng Single Linked List:
Trang 6Undo/Redo Stack
Hình 2:
IV Thiết kế giải thuật, CTDL
Giải thuật chính trong bài toán là giải thuật cho hai chức năng Undo và Redo Để sử dụng hai chức năng trên thì cần kết hợp cấu trúc ngăn xếp như
đã giải thích bên trên
a Thiết kế sơ bộ:
Hình 3.
Head
Thêm thay đổi vào
Undo/Redo stack
Xuất dữ liệu khi User nhấn Undo/Redo Undo và Redo
Redo Undo
Trang 7b Thiết kế chi tiết.
Bản chất cách làm việc của Undo và Redo là quay lại công việc vừa thực hiện trước đó, lần lượt cho tới khi đến công việc được thực hiện đầu tiên Đây chính là chức năng chính của cấu trúc Stack: “Vào Sau
Ra Trước” hay “Last In First Out”
Vì vậy chúng em sẽ sử dụng hai Stack là Undo Stack và Redo Stack, mỗi Stack lưu trữ dữ liệu cho từng chức năng cùng tên
Mỗi khi User thực hiện một công việc, nội dung của văn bản cũ sẽ được lưu vào Undo Stack và khi User undo một công việc, công việc
đó sẽ được lưu vào Redo Stack
Trước tiên, chúng ta cần xác định những trường hợp mà khi User thay đổi văn bản thì ta coi đó là một công việc có thể Undo và ta lưu vào Undo Stack (từ giờ ta coi thay đổi đó là một công việc) :
Khi User mở một File có sẵn, nội dung ban đầu của File đó sẽ được lưu vào Undo Stack
Khi User xóa bỏ ký tự, nội dung của văn bản trước sẽ được đẩy vào Undo Stack
Khi User xuống dòng
Khi User dùng chức năng Redo
Tiếp theo là các trường hợp để lưu vào Redo Stack, vì bản chất của chức năng Redo là hoàn lại những gì User vừa undo, thế nên Redo chỉ
có một trường hợp duy nhất được sử dụng:
Khi User undo một công việc, nội dung của văn bản trước sẽ được đẩy vào Redo Stack
Một điểm đặc biệt là sau khi Undo/Redo, nếu User thực hiện một công việc với văn bản, khi đó User sẽ không được redo nữa, và để thực hiên điều này thì dữ liệu trong Redo Stack sẽ bị xóa
Từ những phân tích trên, ta vẽ được lưu đồ xác định một thay đổi có phải là công việc hay không:
Trang 8(Trong lưu đồ sẽ không đề cập đến một số biến , đặc biệt là 2 biến lưu giữ việc User đã sử dụng hai chức năng Undo Và Redo hay không Việc lưu giữ hành động Undo/Redo của User vừa thực hiện sẽ giúp chúng ta xác định được những trường hợp mà khi User thực hiện một thay đổi thì thay đổi đó có được coi là một công việc để lưu vào Undo Stack hay không Tuy nhiên vì tính tổng hợp của lưu đồ nên chúng em sẽ chỉ trình bày phần cài đặt của hai biến này mà không đưa vào lưu đồ)
Yes
Yes
No
Hình 4.
Có sự thay đổi
Văn bản trống Đẩy vào Undo Stack
ND mới ngắn hơn ND cũ/
xuống dòng
User vừa
dùng
Undo/Redo
Trang 9Từ lưu đồ trên, ta chuyển từng phần sang ngôn ngữ C++ sử dụng API
Win32:
Bảng 1.
Kiểm tra văn bản trống (Nếu văn
bản trống thì ta cần đẩy vào Undo
Stack trạng thái trống này, cụ thể
là một string trống “”.)
if (size == 0 ) {
push ( & file_undo, "" );
old_text = "" ; }
Nội dung mới ngắn hơn nội dung
cũ (User xóa ký tự)
if (old_text length () > size) {
string mydata(data);
push ( & file_undo,old_text); }
User vừa dùng một trong hai chức
năng Undo/Redo (Biến
‘redo_parameter’ và
‘undo_parameter’ sẽ đánh dấu liệu
hành động trươc đó của User có
phải là redo hay undo không)
if (old_text length () < size) {
// if user text more after undo/redo, we need to push old_text into undo
if (redo_parameter == true ) {
push ( & file_undo,old_text); redo_parameter = false ; }
if (undo_parameter == true ) {
push ( & file_undo,old_text); undo_parameter = false ; }
{
User xuống dòng if (data[size - 1 ] == ' \n ' )
{
}
Trang 10Và dưới đây là chi tiết cài đặt thuật toán trong C++:
Bảng 2.
case EN_UPDATE : // User changes the document
{
saved = false ;
if (size == 0 )
{
push ( & file_undo, "" );
old_text = "" ;
}
size = GetWindowTextLength (hEdit); // Get size of the curent document
char * data = new char [size + 1 ];
GetWindowTextA (hEdit,data,size + 1 );
data[size] = ' \0 ' ;
if (old_text length () > size)
{
string mydata(data);
push ( & file_undo,old_text);
}
if (old_text length () < size)
{
{
push ( & file_undo, string (data));
}
if (redo_parameter == true )
{
push ( & file_undo,old_text);
redo_parameter = false ;
}
if (undo_parameter == true )
{
push ( & file_undo,old_text);
undo_parameter = false ;
}
clear ( & file_redo);
}
// update the old_text
old_text = string(data);
delete[] data;
Trang 11}
break ;
Đối với việc cài đặt cấu trúc Stack, chúng em cài đặt bằng C++ và sử dụng Singly Linked List với kiểu dữ liệu là ‘String’ trong thư viện “string”
Vì chỉ được tối đa 10 lần Undo/Redo, nên kích thước của Stack chỉ được 10 Node, vì vậy khi ta đẩy công việc vào hai Stack trên, ta cần kiểm tra nếu kích thước của Stack đã bằng 10 thì cần xóa node ở đáy Stack Vì vậy em đã thêm một hàm để xóa node đáy trong thư viện Stack là Leak():
Bảng 3.
string leak (stack *S) {
{
a = a -> next;
} string x = a -> next -> content;
a -> next = NULL ;
}
Tiếp theo là hai lưu đồ để sử dụng hai chức năng là Undo và Redo:
Yes
Undo Stack trống
Không làm gì cả
Đẩy dữ liệu đó vào Redo
Stack Lấy dữ liệu ra và in ra màn
hình
Trang 12No
Hình 6: Lưu Đồ Redo
Đối với việc cài đặt thư viện Stack, ngoài điểm khác biệt là hàm Leak() đã được nêu bên trên thì còn một thay đổi so với cách cài đặt Stack thông thường đó là trong hàm Push() dùng để đẩy node mới vào Stack, nhóm em thêm một điều kiện nếu chiều dài của Stack đã bằng 10 thì node đáy Stack
sẽ bị đẩy ra nhường chỗ cho node mới, ở đây sử dụng hàm Leak(), giải thuật như sau:
If Length of Stack == 10:
Leak(Stack);
Ngoài ra không có gì khác biệt nên việc cài đặt thư viện Stack sẽ không được trình bày chi tiết trong báo cáo, tuy nhiên việc trình bày các thuật toán của Undo và Redo dưới ngôn ngữ lập trình cụ thể sẽ sử dụng các hàm
Không làm gì
Đẩy dữ liệu vào Undo
stack
Lấy dữ liệu ra và in ra
màn hình
Redo stack trống
Trang 13của thư viện Stack(“file_stack.h”), khi đó các hàm được sử dụng sẽ được ghi chú
Dưới đây là cài đặt chi tiết cho hai function Undo và Redo trên:
void MainWindow:: redo ()
{
if ( isempty (file_redo) == false ) // if redo stack is empty, we don’t do anything
{
redo_parameter = true ; // keep track of redo condition
size = GetWindowTextLength (hEdit); //get size of the current document
char * undo_data = new char [size + 1 ];
// get the content of the current document
GetWindowTextA (hEdit,undo_data,size + 1 );
// we use dynamic memmories so we need to set last element '\0' to finish the string
undo_data[size] = ' \0 ' ;
string a(undo_data);
// Everytime we redo, we need to push the old text to
'file_undo' so that we can undo what we've just redo
push ( & file_undo,a); // push the old text into Undo Stack, a function of ‘file_stack.h’
string data = pop ( & file_redo); // Get the content for redo from Redo Stack, this is a function of ‘file_stack.h’
old_text = data;
int len = data length ();
char * mydata = new char (len + 1 );
// this is how we conver 'string' into 'char', becasue
SetWindowText don't use 'string' type
strcpy (mydata,data c_str ());
// we use dynamic memmories so we need to set last element '\ 0' to finish the string
mydata[len] = ' \0 ' ;
// display the redo text
SetWindowTextA (hEdit,mydata);
// update size of the text becasue "SetWindowText()" doesn't send EN_UPDATE measage(means we can update size in case : EN_UPDATE)
size = GetWindowTextLength (hEdit);
delete[] mydata;
delete[] undo_data;
}
Trang 14Bảng 4: Hàm Redo
Bảng 5: Hàm Undo
void MainWindow:: undo ()
{
// Check if the stack 'file_undo'' is empty
if ( isempty (file_undo) == false )
{
// we set undo_parameter to be true so that if user text anything after undo, we know to push the old_text into stack
file_undo
undo_parameter = true ;
size = GetWindowTextLength (hEdit); //Get size of the current document
char * redo_data = new char [size + 1 ];
// Get the content of the current document
GetWindowTextA (hEdit,redo_data,size + 1 );
redo_data[size] = ' \0 ' ;
string a(redo_data);
// Everytime we undo, we neen to push the old_text to
file_redo so that we can re do what we've just undo
push ( & file_redo,a); // A function of ‘file_stack.h’
string data = pop ( & file_undo); // A function of ‘file_stack.h’
old_text = data;
int len = data length ();
char * mydata = new char [len + 1 ];
strcpy (mydata,data c_str ()); // this is how we conver 'string' into 'char', becasue SetWindowText don't use 'string' type
// we use dynamic memmories so we need to set last element '\ 0' to finish the string
mydata[len] = ' \0 ' ;
SetWindowTextA (hEdit,mydata); // display the undo content
delete[] mydata;
delete[] redo_data;
}
}
Trang 15V Phân tích giải thuật
a) Phân tích tính đúng đắn của giải thuật
Cho tới hiện tại, giải thuật trên đã được nhóm em kiểm tra bằng nhiều loại ví dụ với nhiều kích thước văn bản khác nhau và kết quả đạt được như ý
Trong quá trình sử dụng nếu chương trình gặp phải vấn đề mới thì nhóm em sẽ tiếp tục nghiên cứu cách xử lý
b) Xác định độ phức tạp của giải thuật
Ngoài vòng lặp chính để chạy chương trình, nhóm em chỉ sử dụng một dạng vòng lặp duy nhất trong quá trình thực hiện thuật toán là ở trong hàm tìm chiều dài length(stack*) và hàm loại bỏ node đáy: leak(stack*S) của thư viện Stack(“file_stack.h”):
Hình 7.
Dễ thấy chỉ có một vòng lặp duy nhất ở các hàm trên, vì vậy nếu chiều dài của stack là n thì ta có độ phức tạp của giải thuật là : O(n)
VI Mô tả triển khai trên ngôn ngữ lập trình cụ thể.
Trang 16Sau đây là những hình ảnh của chương trình sau khi hoàn thành cơ bản:
Nhìn từ trên xuống là lần lượt tên của chương trình, thanh công cụ làm việc, và trang soạn thảo
Nhóm em đặt tên chương trình là TDText (shortcut cho Thinh Do Text)
Trên thanh công cụ hiện tại có 4 chức năng: File, Undo, Redo và Help
Hình 8: Màn hình chính
Trong File thì gồm có 3 chức năng: Save, Open File và Exit:
Save: Lưu văn bản
Open File: cửa sổ tìm File sẽ mở ra, người dùng sẽ chọn File.txt
Exit: Ép chương trình thoát, chương trình sẽ dừng ngay cả khi văn bản chưa được lưu