我想要用 wxWidgets 寫一個程式,能夠掃瞄整個目錄結構,然後把不需要的檔案刪掉。掃瞄這件事情可以用遞迴來做:
void MainFrame::RecursiveClean(const wxString& dirname) {
wxDir dir(dirname);
if (!dir.IsOpened()) {
return;
}
wxString name;
wxFileName fn(dirname, "");
bool found = dir.GetFirst(&name);
while (found) {
wxString fullpath = fn.GetPathWithSep() + name;
if (wxDirExists(fullpath)) {
if (delete_folder_patterns_.Match(name)) {
// ...delete folder...
} else {
RecursiveClean(fullpath);
}
} else if (wxFileExists(fullpath)) {
if (delete_file_patterns_.Match(name)) {
// ...delete file...
}
}
found = dir.GetNext(&name);
}
}
Code language: C++ (cpp)
但掃瞄的過程很花時間。如果我們就直接這樣硬幹,在掃瞄的過程中,wxWidgets 就沒有辦法處理事件,導致整個 UI 就會停在那邊沒有反應。所以我們要想個方法去解決。
利用 wxEVT_IDLE
其實最終極的方法應該是把掃瞄放到另一個 thread 去做;但光是想到 thread 之間要怎麼妥善傳遞訊息就頭皮發麻。所以就想說先試著在 wxIdleEvent
的 handler 裡面做。
wxWidgets 的系統在沒有其他事件要處理的時候,就會送出 wxIdleEvent
。但預設的行為,是「從 busy 變成 idle」時,才會送一次 event,而不會連續送出。所以,如果我們希望 wxWidgets 持續送出的話,可以在 wxIdleEvent
的 handler 裡,呼叫 wxIdleEvent::RequestMore()
,這樣 wxWidgets 就會再送出下一個 wxIdleEvent
。
另外,為了要把控制權還給系統,我們必須要把任務細切:每一次呼叫 wxIdleEvent
的 handler 的時候,只執行任務的一小部份就結束函式,讓 wxWidgets 有機會處理其他的事件。通常這會是最累的工作。如果原本的任務是以迴圈方式執行還好;像現在這個例子,用到遞迴,就有點麻煩了。
我使用的方法,是利用 std::vector
模擬 stack,記錄正在處理的目錄。Stack 的最上層是目前正在處理的目錄。當程式在這個目錄中掃到子目錄、要往裡面深入時,就把要處理的目錄再往 stack 上層塞;等到最上層的目錄都處理完了,就把它 pop 出來,繼續處理前一層目錄。其實相當於模擬遞迴的行為。
Pseudo code 如下:
class MyFrame::wxFrame() {
std::vector<Directory> stack;
//....
void ProcessSingleItem(void);
void OnIdle(wxIdleEvent& evt);
}
void MyFrame::OnIdle(wxIdleEvent& evt) {
if (ProcessSingleItem()) {
Unbind(wxEVT_IDLE, &CleanStatusDialog::OnIdle, this);
} else {
// Add this for more performance
evt.RequestMore();
}
}
// Returns true if scanning task is done.
void MyFrame::ProcessSingleItem(void) {
if (stack.empty()) {
return true;
}
wxString item_name;
Directory& top = stack.back();
bool found = top.FindNextItem(&item_name);
if (found) {
wxString full_path = top.GetName() + item_name;
if (is_dir(full_path)) {
if (delete_folder_patterns_.Match(full_path)) {
// ...delete folder...
} else {
Directory new_dir = Directory(full_path);
stack.push_back(new_dir);
}
} else if (is_file(fullpath)) {
if (delete_file_patterns_.Match(full_path)) {
// ...delete file...
}
}
} else {
stack.pop_back();
}
return false;
}
Code language: C++ (cpp)
之所以用 pseudo code 呈現、而且用 Directory
這個「假的」class 而不是直接用 wxDir
,是因為還有幾個實作上的問題要解決:
wxDir
物件無法被複製(它的 copy constructor 被隱藏起來了),所以無法直接用std::vector
儲存管理。解決的方法是改存指標,也就是把stack
的型別改成std::vector<wxDir*>
,或是std::vector<std::unique_ptr<wxDir>>
。- 如果第一點是採用
unique_ptr<wxDir>
的話,那在呼叫std::vector<>::push_back()
的時候,要使用std::move()
。例如:auto new_dir = make_unique<wxDir>(full_path); stack.push_back(move(new_dir));
- 當我們想透過
wxDir
取得目錄中的檔案(或子目錄)的名稱時,我們一開始必須先用wxDir::GetFirst()
取得第一個檔名,然後改用wxDir::GetNext()
依次取得後面的檔名。因此我們必須有額外的機制記錄這是不是第一次呼叫。 - 如果新的
wxDir
物件建立失敗的話,應該要能立即終止整個掃瞄工作。
使用 wxThread 執行掃瞄任務
wxWidgets 提供了兩種不同的 thread 模型:detached(預設)和 joinable。我採用 detached thread 來實作。不過 detached thread 在使用上有幾個需要注意的地方:
- Sub thread 在執行結束後會自我銷毀(也就是自己
delete
自己)。雖然好處是不用 main thread 操心,但若是我們需要在 main thread 中保留一份指向這個 thread 的指標,那我們就必須非常小心,因為這個指標隨時可能因為 thread 已經執行結束而失效。
我們的解決方法,是讓 sub thread 也保留一份指向 parent 的指標,然後在 thread 的解構式中,想辦法想自己的指標清為nullptr
。 - 當 main thread 想要透過 thread 的指標去存取時(例如去呼叫
wxThread::Delete()
,通知 thread 必須提前結束),必須要用 critical section 保護好,否則可能會發生 data racing 的問題。 - Sub thread 要傳遞訊息給 main thread,可以利用
-
wxEvtHandler::QueueEvent()
傳遞標準(或自定義)的事件,有資料的話可以放在事件物件的欄位中。但要特別小心wxString
物件,因為wxString
內部使用 reference count 技術,並不符合 thread-safe 的規範。 - wxWidgets 3.0 之後提供了
wxEvtHandler::CallAfter()
這個函式,可以直接傳入成員函式的指標(以及簡單的參數)、或是函式物件。等到控制權回到 main thread 時,main thread 才會去執行這個成員函式。
-
Thread 這個主題實在是太龐大。再找時間寫另一篇文章來討論好了。