Skip to content Skip to footer

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 по-русски.

Leave a comment

0.0/5