Эх если бы вы знали сколько редакторов не могут корректно делать поиск и замену, особенно касательно неанглийского языка…
На прикручивания поиска&замены, для своего проекта на 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 по-русски.