Эх если бы вы знали сколько редакторов не могут корректно делать поиск и замену, особенно касательно неанглийского языка…
На прикручивания поиска&замены, для своего проекта на wxWidgets я убил пару дней. И вот теперь когда это кое-как работает, хочу поделится с вами. В open source проектах не удалось найти реализацию поиска для wxTextCtrl
или wxRichTextCtrl
, кое-какой код удалось найти на форуме, но его работоспособность оставляла желать очень и очень лучшего. Видимо все серьозные программисты для реальных проектов используют обертки над scintill
‘ой. Так что пришлось писать практически с нуля.
Итак поехали.
В меню добавляем пункты Find, Find Next, Replace и соответвующие обработчики в нашу EVENT_TABLE
// Сообщения меню EVT_MENU( wxID_FIND, FlashnoteFrame::OnFindClick ) EVT_MENU( ID_FindNext, FlashnoteFrame::OnFindNextClick ) EVT_MENU( wxID_REPLACE, FlashnoteFrame::OnReplaceClick ) // Сообщения специфические для стандартного диалога поиска и замены EVT_FIND( wxID_ANY, FlashnoteFrame::OnFind ) EVT_FIND_NEXT( wxID_ANY, FlashnoteFrame::OnFind ) EVT_FIND_CLOSE( wxID_ANY, FlashnoteFrame::OnFindClose ) EVT_FIND_REPLACE( wxID_ANY, FlashnoteFrame::OnReplace ) EVT_FIND_REPLACE_ALL( wxID_ANY, FlashnoteFrame::OnReplaceAll )
В заголовочный файл добавляем такой блок
//********* FIND AND REPLACE ***************************************** void OnFindClick( wxCommandEvent& event ); void OnFindNextClick( wxCommandEvent& event ); void OnReplaceClick( wxCommandEvent& event ); void OnFind( wxFindDialogEvent& event ); bool OnFind( bool restart=false); void OnFindClose( wxFindDialogEvent& event ); void OnReplace(wxFindDialogEvent& event); bool OnReplace(); void OnReplaceAll(wxFindDialogEvent& event); wxFindReplaceData m_findData; wxFindReplaceDialog * m_findDialog; // Specify what kind of wxFindReplaceDialog we have for search or for replace bool IsReplaceDlg; // Don't show MessageBox when can't found text when user want // to ReplaceAll if at least one replace was made И не идем по кругу! int IsReplaceAll;
Ну и теперь пишем собственно код реализующий данную функциональность
void FlashnoteFrame::OnFindClick( wxCommandEvent& event ) { if ( NULL == m_findDialog ) { m_findDialog = new wxFindReplaceDialog( this, &m_findData, _("Find"), wxFR_NOWHOLEWORD ); m_findDialog->Centre( wxCENTRE_ON_SCREEN | wxBOTH ); IsReplaceDlg=false; } m_Note->SetFocus(); m_findDialog->Show( true ); } void FlashnoteFrame::OnReplaceClick( wxCommandEvent& event ) { if ( NULL == m_findDialog ) { m_findDialog = new wxFindReplaceDialog( this, &m_findData, _("Find & Replace"), wxFR_REPLACEDIALOG | wxFR_NOWHOLEWORD); m_findDialog->Centre( wxCENTRE_ON_SCREEN | wxBOTH ); IsReplaceDlg=true; } m_Note->SetFocus(); m_findDialog->Show( true ); } void FlashnoteFrame::OnFindClose( wxFindDialogEvent& event ) { m_findDialog->Destroy(); m_findDialog = NULL; } void FlashnoteFrame::OnFindNextClick( wxCommandEvent& event ) { if(m_findData.GetFindString().IsEmpty()) { wxCommandEvent t; OnFindClick(t); } else OnFind(); } void FlashnoteFrame::OnFind( wxFindDialogEvent& event ) { OnFind(); } // Return true if we found text - need for ReplaceALL bool FlashnoteFrame::OnFind(bool restart) { if (!IsReplaceDlg) { // Only for FIND dlg bool HideFindDialogAfterFirstFing=true; if (HideFindDialogAfterFirstFing && m_findDialog) { // only if user want and our dlg is alive wxFindDialogEvent t; OnFindClose(t); m_Note->SetFocus(); } } wxString find = m_findData.GetFindString(); int flags = m_findData.GetFlags(); bool forward(flags & wxFR_DOWN); int LastPosition=m_Note->GetLastPosition(); long m_foundPos, StartFromBack, StartFromForward; if (restart) { StartFromForward=0; StartFromBack=LastPosition; } else { // First time m_Note->GetSelection(&StartFromBack, &StartFromForward); // If we have not selection, wxTextCtrl return // StartFromBack=StartFromForward=Current_Insertion_Point, // but wxRichTextCtrl return -2,-2 if (StartFromBack<0) StartFromBack=StartFromForward=m_Note->GetInsertionPoint(); // Some optimization if (IsReplaceAll==0) { if (forward && (StartFromForward==LastPosition)) StartFromForward=0; if (!forward && (StartFromBack==0)) StartFromBack=LastPosition; // если первый раз ищем вперед с первой позиции или назад с // последней - то незачем второй раз проходить if ( (forward && (StartFromForward==0)) || ( !forward && (StartFromBack==LastPosition)) ) restart=true; } // End of Some optimization } wxString string = forward ? m_Note->GetRange(StartFromForward, LastPosition) : m_Note->GetRange(0, StartFromBack); if (flags & wxFR_MATCHCASE) m_foundPos = forward ? string.find(find) : string.rfind(find); else { wxString string_lwr(string), find_lwr(find); BegUtils::StrMakeLowerUniversal(string_lwr); BegUtils::StrMakeLowerUniversal(find_lwr); m_foundPos = forward ? string_lwr.find(find_lwr) : string_lwr.rfind(find_lwr); //m_foundPos = forward ? //string.Lower().find(find.Lower()) : //string.Lower().rfind(find.Lower()); } // Начинаем с начала (конца) если не нашли и сюда зашли первый раз // (restart==false) И это не заменить все if (m_foundPos==-1) { if (restart || (IsReplaceAll>0)) { // уже по второму разу прошли или зашли из ReplaceAll if (IsReplaceAll<2) wxMessageBox(wxString::Format( _("Cannot find \"%s\""), find), wxT("Flashnote"), wxOK|wxICON_INFORMATION, m_findDialog); return false; //Not found } else return OnFind(true); // Ищем с начала и возвращяем нашли или нет } long startPos = forward ? StartFromForward + m_foundPos : m_foundPos; long endPos = startPos + find.length(); m_Note->SetSelection(startPos, endPos); return true; } void FlashnoteFrame::OnReplace(wxFindDialogEvent& event) { OnReplace(); } bool FlashnoteFrame::OnReplace() { wxString FindText(m_findData.GetFindString()); wxString SelText(m_Note->GetStringSelection()); if (!SelText.IsEmpty()) { // We have selected text, if is it equal our FindText - replace it // bool IsTextToReplace = m_findData.GetFlags() & // wxFR_MATCHCASE ? SelText==FindText : // SelText.Lower()==FindText.Lower(); // if (IsTextToReplace) m_Note->WriteText(m_findData.GetReplaceString()); if (! (m_findData.GetFlags() & wxFR_MATCHCASE)) { BegUtils::StrMakeLowerUniversal(SelText); BegUtils::StrMakeLowerUniversal(FindText); } if (SelText==FindText) { // this line is needed only for wxRichTextCtrl and needed not for wxTextCtrl m_Note->DeleteSelection(); m_Note->WriteText(m_findData.GetReplaceString()); } } return OnFind(); } void FlashnoteFrame::OnReplaceAll(wxFindDialogEvent& event) { //Note: We can't just use wxString::Replace() for //replace, if we have handle wxFR_MATCHCASE flag IsReplaceAll=1; // start ReplaceAll while (OnReplace()) ++IsReplaceAll; if (IsReplaceAll>1) // we have replaced something wxMessageBox(wxString::Format(_("Total Replacement: %i"), IsReplaceAll-1), wxT("Flashnote"), wxOK|wxICON_INFORMATION, m_findDialog); IsReplaceAll=0; // stop ReplaceAll }
Основная работа идет в функции OnFind(bool restart/*=false*/)
и OnReplace()
, все остальное фактически обвязка вокруг этих двух функций.
Код взят из реального приложения – программы для быстрых заметок (будет использоватся с v3.0) и отражает мое субьективное представление как должен вести себя диалог вставки замены. Из плюсов – неплохо оттестирован, с учетом мелочей, но если найдете проблемы – велком в комментарии.
Особенности:
- Поиск идет циклически по кругу, то есть по достижению конца (начала) документа, автоматически переходит на начало (конец) и продолжает поиск.
- Замена в ручном режиме ведет себя точно также, в режиме Replace ALL документ просматривается только до конца.
- После нахождения первого вхождения искомой строки диалог поиска скрывается, дальнейший поиск идет с использованием F3 (шоткат для меню Find Next). Изменить это можно изменив строчку
bool HideFindDialogAfterFirstFing=true;
в функцииOnFind(bool restart/*=false*/)
- Для приведения строки к нижнему регистру используется самописная функция
StrMakeLowerUniversal()
(см. ниже). При использовании стандартнойLower()
регистронезависимый поиск будет работать только для латиницы.
Функция StrMakeLowerUniversal(wxString & data) преобразует строку к нижнему регистру, работая со строками на любом языке, в отличии от Lower()
которая понимает только латиницу. Из минусов – поддержка только MSW.
void BegUtils::StrMakeLowerUniversal(wxString & data)
{
#ifdef __WIN32__
int len=data.length()+1;
wxChar * buf = new wxChar[len];
wxStrcpy(buf, data.c_str());
CharLower(buf);
data=buf;
delete [] buf;
#else
data.MakeLower();
#endif
Успехов в написании безглючных приложений:)
Об авторе: Тюшков Николай, автор программы для мгновенной вставки текста и ряда других программ. Так же я веду несколько авторских блогов, в частности блог мысли про wx-widgets по-русски.