Categories: wxWidgets

Find & Replace для wxWidgets-приложений

Эх если бы вы знали сколько редакторов не могут корректно делать поиск и замену, особенно касательно неанглийского языка…

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

T-Rex

Share
Published by
T-Rex

Recent Posts

Разработка кроссплатформенных модульных приложений на C++ с библиотекой wxWidgets

Введение Уже долгое время не пишу статьи о разработке, хотя сам процесс написания мне очень…

10 years ago

wxWidgets App With Plugins (Windows/Linux/Mac) – Sample Source Code

I can see that there is still a lot of topics at wxWidgets forums related…

11 years ago

wxToolBox is Now Open-Source!

I've just published the source code of wxToolBox component and a couple of sample apps at…

11 years ago

Microsoft Kinect Helper Library and Sample for wxWidgets

Microsoft released their Kinect SDK several days ago. So, for those wxWidgets developers who are…

13 years ago

wxJSON 1.1.0 Released

JSON (JavaScript Object Notation) is a lightweight data-interchange format. It is easy for humans to…

14 years ago

wxRuby. Оно даже работает!

Вдохновленнный читаемой нынче книгой My Job Went to India: 52 Ways to Save Your Job…

15 years ago